diff --git a/pkg/database/mediascanner/mediascanner.go b/pkg/database/mediascanner/mediascanner.go index 696f1a65..f1f1dd99 100644 --- a/pkg/database/mediascanner/mediascanner.go +++ b/pkg/database/mediascanner/mediascanner.go @@ -217,6 +217,10 @@ func GetSystemPaths( for i := range launchers { // Skip filesystem scanning for launchers that don't need it if launchers[i].SkipFilesystemScan { + log.Trace(). + Str("launcher", launchers[i].ID). + Str("system", system.ID). + Msg("skipping filesystem scan for launcher") continue } for _, folder := range launchers[i].Folders { @@ -226,6 +230,13 @@ func GetSystemPaths( } } + log.Trace(). + Str("system", system.ID). + Int("launchers", len(launchers)). + Strs("folders", folders). + Strs("rootFolders", validRootFolders). + Msg("resolving system paths") + // check for / for _, gf := range validRootFolders { for _, folder := range folders { @@ -463,6 +474,14 @@ func GetFiles( return nil, fmt.Errorf("failed to walk directory %s: %w", realPath, err) } walkElapsed := time.Since(walkStartTime) + + results, err := stack.get() + if err != nil { + return nil, err + } + + allResults = append(allResults, *results...) + log.Debug(). Str("system", systemID). Str("path", realPath). @@ -488,13 +507,6 @@ func GetFiles( Msg("directory walk took longer than expected - large directory or slow storage") } - results, err := stack.get() - if err != nil { - return nil, err - } - - allResults = append(allResults, *results...) - // change root back to symlink if realPath != path { for i := range allResults { @@ -918,17 +930,35 @@ func NewNamesIndex( } } - // for each system launcher in a platform, run the results through its - // custom scan function if one exists + // Run each system launcher's custom scanner if one exists. + // + // SkipFilesystemScan launchers (e.g. RetroBat, Kodi) generate results + // independently — they don't filter/enrich the shared file list, so they + // receive empty input and their results are *appended* to files. This + // prevents an independent scanner that ignores its input from wiping out + // files discovered by other launchers or the filesystem walk. + // + // Non-skip launchers (e.g. Batocera gamelist.xml enrichment) act as a + // pipeline: they receive the current files and their output *replaces* + // files, allowing them to filter, reorder, or add metadata. launchers := helpers.GlobalLauncherCache.GetLaunchersBySystem(k) for i := range launchers { l := &launchers[i] if l.Scanner != nil { log.Debug().Msgf("running %s scanner for system: %s", l.ID, systemID) var scanErr error - files, scanErr = l.Scanner(ctx, cfg, systemID, files) + if l.SkipFilesystemScan { + // Isolated: scanner gets empty input, results accumulated + var independent []platforms.ScanResult + independent, scanErr = l.Scanner(ctx, cfg, systemID, nil) + if scanErr == nil { + files = append(files, independent...) + } + } else { + // Pipeline: scanner filters/enriches existing files + files, scanErr = l.Scanner(ctx, cfg, systemID, files) + } if scanErr != nil { - // Check if this is a cancellation error if errors.Is(scanErr, context.Canceled) { return handleCancellationWithRollback(ctx, db, "Media indexing cancelled during custom scanner") } diff --git a/pkg/database/mediascanner/mediascanner_test.go b/pkg/database/mediascanner/mediascanner_test.go index 74cd2bd2..1bf1e3be 100644 --- a/pkg/database/mediascanner/mediascanner_test.go +++ b/pkg/database/mediascanner/mediascanner_test.go @@ -1244,6 +1244,418 @@ func TestAnyScannerProgressUpdates(t *testing.T) { } } +// TestGetSystemPaths_CustomLauncherAbsolutePaths tests that GetSystemPaths returns +// valid paths for a custom launcher with absolute Folders pointing to real temp directories. +func TestGetSystemPaths_CustomLauncherAbsolutePaths(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directories to simulate custom launcher media dirs + ps2Dir := t.TempDir() + + // Create test files in the directory + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "game1.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "game2.iso"), []byte("test"), 0o600)) + + // Create a custom launcher with absolute path Folders + launcher := platforms.Launcher{ + ID: "custom-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{ps2Dir}, + Extensions: []string{".iso", ".bin"}, + } + + // Create config + fs := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fs, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return([]platforms.Launcher{launcher}) + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Call GetSystemPaths with no root folders (custom launchers use absolute paths) + systems := []systemdefs.System{{ID: systemdefs.SystemPS2}} + results := GetSystemPaths(cfg, platform, []string{}, systems) + + // The custom launcher's absolute folder should be resolved + require.Len(t, results, 1, "Should find exactly one path for PS2 custom launcher") + assert.Equal(t, systemdefs.SystemPS2, results[0].System.ID) + assert.Equal(t, ps2Dir, results[0].Path) +} + +// TestGetFiles_CustomLauncherMatchesFiles is the critical reproduction test: +// it verifies that GetFiles() actually finds and matches files when walking +// a directory from a custom launcher with absolute paths. +func TestGetFiles_CustomLauncherMatchesFiles(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with test files + ps2Dir := t.TempDir() + testFiles := []string{"Final Fantasy X.iso", "Metal Gear Solid 3.iso", "Shadow of the Colossus.bin"} + for _, f := range testFiles { + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, f), []byte("test"), 0o600)) + } + // Also create a file that should NOT match + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "readme.txt"), []byte("test"), 0o600)) + + // Create a custom launcher with absolute path + launcher := platforms.Launcher{ + ID: "custom-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{ps2Dir}, + Extensions: []string{".iso", ".bin"}, + } + + // Create config + fs := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fs, t.TempDir()) + require.NoError(t, err) + + // Create mock platform - RootDirs is needed by PathIsLauncher for root-relative folder matching + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return([]platforms.Launcher{launcher}) + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Call GetFiles with the custom launcher's directory + ctx := context.Background() + files, err := GetFiles(ctx, cfg, platform, systemdefs.SystemPS2, ps2Dir) + require.NoError(t, err) + + // Should find all matching files (.iso and .bin) but not .txt + assert.Len(t, files, 3, "Should find 3 matching files (.iso and .bin)") + + // Verify the right files were found + foundFiles := make(map[string]bool) + for _, f := range files { + foundFiles[filepath.Base(f)] = true + } + assert.True(t, foundFiles["Final Fantasy X.iso"]) + assert.True(t, foundFiles["Metal Gear Solid 3.iso"]) + assert.True(t, foundFiles["Shadow of the Colossus.bin"]) + assert.False(t, foundFiles["readme.txt"], "Non-matching extension should be excluded") +} + +// TestGetFiles_CustomLauncherNestedDirectories verifies that GetFiles walks +// subdirectories within a custom launcher's absolute path. +func TestGetFiles_CustomLauncherNestedDirectories(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with nested structure + ps2Dir := t.TempDir() + rpgDir := filepath.Join(ps2Dir, "RPG") + actionDir := filepath.Join(ps2Dir, "Action") + require.NoError(t, os.MkdirAll(rpgDir, 0o750)) + require.NoError(t, os.MkdirAll(actionDir, 0o750)) + + require.NoError(t, os.WriteFile(filepath.Join(rpgDir, "FFX.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(actionDir, "DMC3.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "root_game.iso"), []byte("test"), 0o600)) + + // Create a custom launcher with absolute path + launcher := platforms.Launcher{ + ID: "custom-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{ps2Dir}, + Extensions: []string{".iso"}, + } + + // Create config + fs := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fs, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return([]platforms.Launcher{launcher}) + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Call GetFiles + ctx := context.Background() + files, err := GetFiles(ctx, cfg, platform, systemdefs.SystemPS2, ps2Dir) + require.NoError(t, err) + + assert.Len(t, files, 3, "Should find files in root and nested directories") +} + +// TestNewNamesIndex_CustomLauncherE2E is a full end-to-end test: +// custom launcher → GetSystemPaths → GetFiles → AddMediaPath → verify DB has entries. +func TestNewNamesIndex_CustomLauncherE2E(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with test files + ps2Dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "Final Fantasy X.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(ps2Dir, "Metal Gear Solid 3.bin"), []byte("test"), 0o600)) + + // Create a custom launcher with absolute path + launcher := platforms.Launcher{ + ID: "custom-ps2-e2e", + SystemID: systemdefs.SystemPS2, + Folders: []string{ps2Dir}, + Extensions: []string{".iso", ".bin"}, + } + + // Create config + fsHelper := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fsHelper, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return([]platforms.Launcher{launcher}) + + // Use real database + db, cleanup := testhelpers.NewTestDatabase(t) + defer cleanup() + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Run the full indexer + systems := []systemdefs.System{{ID: systemdefs.SystemPS2}} + filesIndexed, err := NewNamesIndex(context.Background(), platform, cfg, systems, db, func(IndexStatus) {}) + require.NoError(t, err) + + assert.Equal(t, 2, filesIndexed, "Should have indexed 2 files from custom launcher") + + // Verify files are actually in the database + mediaEntries, err := db.MediaDB.GetMediaBySystemID(systemdefs.SystemPS2) + require.NoError(t, err) + assert.Len(t, mediaEntries, 2, "Database should contain 2 media entries for PS2") + + // Verify the paths are correct + foundPaths := make(map[string]bool) + for _, entry := range mediaEntries { + foundPaths[filepath.Base(entry.Path)] = true + } + assert.True(t, foundPaths["Final Fantasy X.iso"], "Should find Final Fantasy X.iso in DB") + assert.True(t, foundPaths["Metal Gear Solid 3.bin"], "Should find Metal Gear Solid 3.bin in DB") +} + +// TestNewNamesIndex_MixedSkipFilesystemScanAndCustomLauncher tests the scenario where +// a SkipFilesystemScan launcher (e.g., RetroBat) AND a custom launcher both target +// the same system (e.g., PS2). Verifies the custom launcher's files still get indexed. +func TestNewNamesIndex_MixedSkipFilesystemScanAndCustomLauncher(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with test files for the custom launcher + customDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(customDir, "game1.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(customDir, "game2.mdf"), []byte("test"), 0o600)) + + // SkipFilesystemScan launcher with a Scanner that ignores its input and + // returns empty — simulates RetroBat when gamelist.xml is missing. + skipLauncher := platforms.Launcher{ + ID: "retrobat-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{"ps2"}, + Extensions: []string{".iso", ".bin", ".chd"}, + SkipFilesystemScan: true, + Scanner: func(_ context.Context, _ *config.Instance, _ string, + _ []platforms.ScanResult, + ) ([]platforms.ScanResult, error) { + return nil, nil + }, + } + + // Custom launcher with absolute path and additional extensions + customLauncher := platforms.Launcher{ + ID: "custom-ps2-emulator", + SystemID: systemdefs.SystemPS2, + Folders: []string{customDir}, + Extensions: []string{".iso", ".mdf"}, + } + + // Create config + fsHelper := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fsHelper, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return( + []platforms.Launcher{skipLauncher, customLauncher}) + + // Use real database + db, cleanup := testhelpers.NewTestDatabase(t) + defer cleanup() + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Run the full indexer + systems := []systemdefs.System{{ID: systemdefs.SystemPS2}} + filesIndexed, err := NewNamesIndex(context.Background(), platform, cfg, systems, db, func(IndexStatus) {}) + require.NoError(t, err) + + // The custom launcher's files should be indexed even though + // the SkipFilesystemScan launcher also targets PS2 + assert.Equal(t, 2, filesIndexed, "Should have indexed 2 files from custom launcher") + + // Verify files are actually in the database + mediaEntries, err := db.MediaDB.GetMediaBySystemID(systemdefs.SystemPS2) + require.NoError(t, err) + assert.Len(t, mediaEntries, 2, "Database should contain 2 media entries for PS2") + + // Verify both file types were indexed + foundPaths := make(map[string]bool) + for _, entry := range mediaEntries { + foundPaths[filepath.Base(entry.Path)] = true + } + assert.True(t, foundPaths["game1.iso"], "Should find .iso file in DB") + assert.True(t, foundPaths["game2.mdf"], "Should find .mdf file in DB") +} + +// TestNewNamesIndex_IndependentScannerDoesNotWipeFiles is the critical regression +// test for the scanner isolation fix. A custom launcher provides filesystem files, +// and a SkipFilesystemScan launcher has a Scanner that ignores its input and returns +// its own results. Both launchers' files must end up in the DB. +func TestNewNamesIndex_IndependentScannerDoesNotWipeFiles(t *testing.T) { + // Cannot use t.Parallel() - modifies shared GlobalLauncherCache + + // Create temp directory with test files for the custom (filesystem) launcher + customDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(customDir, "game1.iso"), []byte("test"), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(customDir, "game2.bin"), []byte("test"), 0o600)) + + // SkipFilesystemScan launcher whose Scanner ignores input and generates its own + // results (e.g. Kodi API, gamelist.xml with independent paths). + skipLauncher := platforms.Launcher{ + ID: "independent-scanner", + SystemID: systemdefs.SystemPS2, + SkipFilesystemScan: true, + Scanner: func(_ context.Context, _ *config.Instance, _ string, + _ []platforms.ScanResult, + ) ([]platforms.ScanResult, error) { + return []platforms.ScanResult{ + {Name: "Scanner Game A", Path: "/virtual/scanner-game-a.iso"}, + {Name: "Scanner Game B", Path: "/virtual/scanner-game-b.iso"}, + }, nil + }, + } + + // Custom launcher with real filesystem files + customLauncher := platforms.Launcher{ + ID: "custom-ps2", + SystemID: systemdefs.SystemPS2, + Folders: []string{customDir}, + Extensions: []string{".iso", ".bin"}, + } + + // Create config + fsHelper := testhelpers.NewMemoryFS() + cfg, err := testhelpers.NewTestConfig(fsHelper, t.TempDir()) + require.NoError(t, err) + + // Create mock platform + platform := mocks.NewMockPlatform() + platform.On("ID").Return("test-platform") + platform.On("Settings").Return(platforms.Settings{}) + platform.On("RootDirs", mock.AnythingOfType("*config.Instance")).Return([]string{}) + platform.On("Launchers", mock.AnythingOfType("*config.Instance")).Return( + []platforms.Launcher{skipLauncher, customLauncher}) + + // Use real database + db, cleanup := testhelpers.NewTestDatabase(t) + defer cleanup() + + // Initialize launcher cache + testLauncherCacheMutex.Lock() + originalCache := helpers.GlobalLauncherCache + testCache := &helpers.LauncherCache{} + testCache.Initialize(platform, cfg) + helpers.GlobalLauncherCache = testCache + defer func() { + helpers.GlobalLauncherCache = originalCache + testLauncherCacheMutex.Unlock() + }() + + // Run the full indexer + systems := []systemdefs.System{{ID: systemdefs.SystemPS2}} + filesIndexed, err := NewNamesIndex(context.Background(), platform, cfg, systems, db, func(IndexStatus) {}) + require.NoError(t, err) + + // Both launchers' files should be indexed: 2 from filesystem + 2 from scanner + assert.Equal(t, 4, filesIndexed, + "Should have indexed 4 files total (2 filesystem + 2 from independent scanner)") + + // Verify files are actually in the database + mediaEntries, err := db.MediaDB.GetMediaBySystemID(systemdefs.SystemPS2) + require.NoError(t, err) + assert.Len(t, mediaEntries, 4, "Database should contain 4 media entries for PS2") + + // Verify all files are present + foundPaths := make(map[string]bool) + for _, entry := range mediaEntries { + foundPaths[filepath.Base(entry.Path)] = true + } + assert.True(t, foundPaths["game1.iso"], "Should find filesystem game1.iso in DB") + assert.True(t, foundPaths["game2.bin"], "Should find filesystem game2.bin in DB") + assert.True(t, foundPaths["scanner-game-a.iso"], "Should find scanner game A in DB") + assert.True(t, foundPaths["scanner-game-b.iso"], "Should find scanner game B in DB") +} + // TestZaparooignoreMarker tests that directories containing a .zaparooignore file // are skipped during media scanning along with all their subdirectories. func TestZaparooignoreMarker(t *testing.T) { diff --git a/pkg/helpers/launchers_test.go b/pkg/helpers/launchers_test.go index 611cb163..9122a7ed 100644 --- a/pkg/helpers/launchers_test.go +++ b/pkg/helpers/launchers_test.go @@ -26,6 +26,7 @@ import ( "github.com/ZaparooProject/zaparoo-core/v2/pkg/platforms" "github.com/ZaparooProject/zaparoo-core/v2/pkg/testing/mocks" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseLifecycle(t *testing.T) { @@ -214,6 +215,36 @@ func TestParseCustomLaunchers_AllUnknownSystems(t *testing.T) { assert.Empty(t, launchers) } +func TestParseCustomLaunchers_AbsolutePathWithSystemID(t *testing.T) { + t.Parallel() + + mockPlatform := mocks.NewMockPlatform() + mockPlatform.On("ID").Return("test") + + customLaunchers := []config.LaunchersCustom{ + { + ID: "ps2-custom", + System: "PS2", + Execute: "pcsx2 [[media_path]]", + MediaDirs: []string{"/emulation/roms/ps2", "/mnt/games/ps2"}, + FileExts: []string{"iso", ".bin", "MDF", " .chd "}, + }, + } + + launchers := ParseCustomLaunchers(mockPlatform, customLaunchers) + + require.Len(t, launchers, 1) + l := launchers[0] + + assert.Equal(t, "ps2-custom", l.ID) + assert.Equal(t, "PS2", l.SystemID, "SystemID should be the canonical ID from LookupSystem") + assert.Equal(t, []string{"/emulation/roms/ps2", "/mnt/games/ps2"}, l.Folders, + "MediaDirs should map directly to Folders") + assert.Equal(t, []string{".iso", ".bin", ".mdf", ".chd"}, l.Extensions, + "FileExts should be dot-prefixed and lowercased") + assert.NotNil(t, l.Launch, "Launch function should be set") +} + func TestFormatExtensions(t *testing.T) { t.Parallel() diff --git a/pkg/helpers/paths.go b/pkg/helpers/paths.go index 61d45222..64387c1d 100644 --- a/pkg/helpers/paths.go +++ b/pkg/helpers/paths.go @@ -139,6 +139,12 @@ func PathIsLauncher( } if !inRoot && !isAbs { + log.Trace(). + Str("launcher", l.ID). + Str("path", path). + Strs("folders", l.Folders). + Strs("rootDirs", rootDirs). + Msg("path not in any launcher folder (root or absolute)") return false } } @@ -154,6 +160,11 @@ func PathIsLauncher( if l.Test != nil { return l.Test(cfg, lp) } + log.Trace(). + Str("launcher", l.ID). + Str("path", path). + Strs("extensions", l.Extensions). + Msg("path extension did not match any launcher extension") return false } diff --git a/pkg/helpers/paths_test.go b/pkg/helpers/paths_test.go index 7ae9e5d2..8f0b22c1 100644 --- a/pkg/helpers/paths_test.go +++ b/pkg/helpers/paths_test.go @@ -527,10 +527,6 @@ func TestGetPathExt(t *testing.T) { } } -// Note: PathIsLauncher tests are in the integration tests (pkg/database/mediascanner) -// to avoid import cycles with the platforms package. The function is thoroughly tested -// through those integration tests which exercise all code paths. - // TestGetPathInfo_VirtualPathEdgeCases tests edge cases in GetPathInfo with virtual paths // that could cause issues with encoding/decoding, parsing, or display func TestGetPathInfo_VirtualPathEdgeCases(t *testing.T) {