@@ -438,6 +438,147 @@ func TestListShims_SkipsCmdFiles(t *testing.T) {
438438 }
439439}
440440
441+ // writeExecutable writes a file with the executable bit set on Unix. The
442+ // content is irrelevant — findExecutables only looks at extension (Windows)
443+ // or the exec bit (Unix) — but a non-zero body keeps file readers happy.
444+ func writeExecutable (t * testing.T , path string ) {
445+ t .Helper ()
446+ if err := os .WriteFile (path , []byte ("x" ), 0755 ); err != nil {
447+ t .Fatalf ("Failed to write %s: %v" , path , err )
448+ }
449+ }
450+
451+ // platformExeName returns name with the OS-appropriate executable extension.
452+ // On Windows the .exe suffix is required for findExecutables to recognize
453+ // the file; on Unix the bare name is used and the exec bit drives detection.
454+ func platformExeName (name string ) string {
455+ if runtime .GOOS == constants .OSWindows {
456+ return name + constants .ExtExe
457+ }
458+ return name
459+ }
460+
461+ func TestDiscoverShimsForVersion_EmptyDir (t * testing.T ) {
462+ versionDir := t .TempDir ()
463+
464+ got := DiscoverShimsForVersion (versionDir )
465+ if len (got ) != 0 {
466+ t .Errorf ("DiscoverShimsForVersion(empty) = %v, want []" , got )
467+ }
468+ }
469+
470+ func TestDiscoverShimsForVersion_MissingVersionDir (t * testing.T ) {
471+ // Caller may pass a path that doesn't exist (e.g., when called before
472+ // the install has moved files into place). The helper must not panic
473+ // or surface an error — it just returns no names.
474+ got := DiscoverShimsForVersion (filepath .Join (t .TempDir (), "does-not-exist" ))
475+ if len (got ) != 0 {
476+ t .Errorf ("DiscoverShimsForVersion(missing) = %v, want []" , got )
477+ }
478+ }
479+
480+ func TestDiscoverShimsForVersion_BinDir (t * testing.T ) {
481+ versionDir := t .TempDir ()
482+ binDir := filepath .Join (versionDir , "bin" )
483+ if err := os .MkdirAll (binDir , 0755 ); err != nil {
484+ t .Fatalf ("mkdir bin: %v" , err )
485+ }
486+
487+ writeExecutable (t , filepath .Join (binDir , platformExeName ("node" )))
488+ writeExecutable (t , filepath .Join (binDir , platformExeName ("npm" )))
489+
490+ got := DiscoverShimsForVersion (versionDir )
491+ want := []string {"node" , "npm" }
492+ if ! reflect .DeepEqual (got , want ) {
493+ t .Errorf ("DiscoverShimsForVersion(bin) = %v, want %v" , got , want )
494+ }
495+ }
496+
497+ // TestDiscoverShimsForVersion_PythonWindowsOnlyRoot models the python-build-
498+ // standalone Windows tarball before ensurepip runs: python.exe and
499+ // pythonw.exe live in the version root and Scripts/ is either absent or
500+ // only contains the upstream .empty placeholder. The discover helper must
501+ // not invent pip / python3 entries that don't exist on disk — that was
502+ // the root cause of issue #269 where install-time shim creation used a
503+ // static provider declaration instead of disk truth.
504+ func TestDiscoverShimsForVersion_PythonWindowsOnlyRoot (t * testing.T ) {
505+ if runtime .GOOS != constants .OSWindows {
506+ t .Skip ("Windows-specific layout (root .exe files)" )
507+ }
508+
509+ versionDir := t .TempDir ()
510+ writeExecutable (t , filepath .Join (versionDir , "python.exe" ))
511+ writeExecutable (t , filepath .Join (versionDir , "pythonw.exe" ))
512+ // Upstream ships an empty Scripts/.empty placeholder; recreate it
513+ // here so the test reflects the exact layout users see and proves
514+ // the helper does not surface the placeholder as a shim.
515+ if err := os .MkdirAll (filepath .Join (versionDir , "Scripts" ), 0755 ); err != nil {
516+ t .Fatalf ("mkdir Scripts: %v" , err )
517+ }
518+ if err := os .WriteFile (filepath .Join (versionDir , "Scripts" , ".empty" ), nil , 0644 ); err != nil {
519+ t .Fatalf ("write .empty: %v" , err )
520+ }
521+
522+ got := DiscoverShimsForVersion (versionDir )
523+ want := []string {"python" , "pythonw" }
524+ if ! reflect .DeepEqual (got , want ) {
525+ t .Errorf ("got %v, want %v" , got , want )
526+ }
527+ }
528+
529+ // TestDiscoverShimsForVersion_PythonWindowsAfterEnsurepip models the
530+ // post-ensurepip state: root has python.exe/pythonw.exe, Scripts/ has
531+ // pip.exe, pip3.exe, and the versioned pip3.14.exe. All five should be
532+ // discovered and the result should be sorted+deduplicated.
533+ func TestDiscoverShimsForVersion_PythonWindowsAfterEnsurepip (t * testing.T ) {
534+ if runtime .GOOS != constants .OSWindows {
535+ t .Skip ("Windows-specific layout (Scripts/*.exe)" )
536+ }
537+
538+ versionDir := t .TempDir ()
539+ writeExecutable (t , filepath .Join (versionDir , "python.exe" ))
540+ writeExecutable (t , filepath .Join (versionDir , "pythonw.exe" ))
541+
542+ scriptsDir := filepath .Join (versionDir , "Scripts" )
543+ if err := os .MkdirAll (scriptsDir , 0755 ); err != nil {
544+ t .Fatalf ("mkdir Scripts: %v" , err )
545+ }
546+ writeExecutable (t , filepath .Join (scriptsDir , "pip.exe" ))
547+ writeExecutable (t , filepath .Join (scriptsDir , "pip3.exe" ))
548+ writeExecutable (t , filepath .Join (scriptsDir , "pip3.14.exe" ))
549+
550+ got := DiscoverShimsForVersion (versionDir )
551+ want := []string {"pip" , "pip3" , "pip3.14" , "python" , "pythonw" }
552+ if ! reflect .DeepEqual (got , want ) {
553+ t .Errorf ("got %v, want %v" , got , want )
554+ }
555+ }
556+
557+ // TestDiscoverShimsForVersion_DedupesAcrossDirs proves the helper unions
558+ // names across bin/ root/ and Scripts/ rather than double-counting. This
559+ // matters because some runtimes (e.g., Ruby on Windows) place the same
560+ // command name in multiple search locations.
561+ func TestDiscoverShimsForVersion_DedupesAcrossDirs (t * testing.T ) {
562+ if runtime .GOOS != constants .OSWindows {
563+ t .Skip ("Multi-dir Windows scan (root + Scripts)" )
564+ }
565+
566+ versionDir := t .TempDir ()
567+ writeExecutable (t , filepath .Join (versionDir , "tool.exe" ))
568+
569+ scriptsDir := filepath .Join (versionDir , "Scripts" )
570+ if err := os .MkdirAll (scriptsDir , 0755 ); err != nil {
571+ t .Fatalf ("mkdir Scripts: %v" , err )
572+ }
573+ writeExecutable (t , filepath .Join (scriptsDir , "tool.exe" ))
574+
575+ got := DiscoverShimsForVersion (versionDir )
576+ want := []string {"tool" }
577+ if ! reflect .DeepEqual (got , want ) {
578+ t .Errorf ("got %v, want %v" , got , want )
579+ }
580+ }
581+
441582func TestRuntimeShims_AllKnownRuntimes (t * testing.T ) {
442583 // Verify all known runtimes have shim mappings
443584 knownRuntimes := []string {"python" , "node" , "ruby" , "go" }
0 commit comments