From 76f50ba89e602db80893325b7da2e2c5fc735be0 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 14 Jun 2026 12:23:33 -0400 Subject: [PATCH] fix(soundpack): resolve bare embedded names --- internal/cli/cli.go | 6 ++ internal/cli/embedded_linux_soundpack_test.go | 65 ++++++++++++++++++- .../cli/embedded_soundpack_discovery_test.go | 23 +++++++ internal/cli/soundpack_helpers.go | 31 +++++++-- 4 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 internal/cli/embedded_soundpack_discovery_test.go diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 93e7a9e..4cd12a2 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -243,6 +243,12 @@ func initializeAudioSystem(cmd *cobra.Command, cli *CLI, cfg *config.Config) err slog.Warn("failed to load embedded platform soundpack from config", "identifier", cfg.DefaultSoundpack, "error", err) } + } else if embeddedIdentifier, ok := embeddedPlatformSoundpackIdentifier(cfg.DefaultSoundpack); ok { + mapper, err = loadEmbeddedPlatformSoundpack(embeddedIdentifier) + if err != nil { + slog.Warn("failed to load embedded platform soundpack from config name", + "name", cfg.DefaultSoundpack, "identifier", embeddedIdentifier, "error", err) + } } else { // Resolve soundpack name to path: if default_soundpack is a name (not a path), // search soundpack_paths for a matching entry diff --git a/internal/cli/embedded_linux_soundpack_test.go b/internal/cli/embedded_linux_soundpack_test.go index 5fbc868..91b0271 100644 --- a/internal/cli/embedded_linux_soundpack_test.go +++ b/internal/cli/embedded_linux_soundpack_test.go @@ -4,14 +4,77 @@ import ( "context" "encoding/json" "os" + "path/filepath" "testing" "claudio.click/internal/cli/testenv" + "claudio.click/internal/config" "claudio.click/internal/hooks" - "claudio.click/internal/sounds" "claudio.click/internal/soundpack" + "claudio.click/internal/sounds" ) +func TestEmbeddedPlatformSoundpackIdentifierRecognizesBarePlatformNames(t *testing.T) { + tests := []struct { + name string + want string + }{ + {name: "windows", want: "embedded:windows.json"}, + {name: "wsl", want: "embedded:wsl.json"}, + {name: "darwin", want: "embedded:darwin.json"}, + {name: "linux", want: "embedded:linux.json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := embeddedPlatformSoundpackIdentifier(tt.name) + if !ok { + t.Fatalf("expected %q to be recognized as a bare embedded soundpack name", tt.name) + } + if got != tt.want { + t.Fatalf("expected %q, got %q", tt.want, got) + } + }) + } +} + +func TestInitializeAudioSystemUsesBareEmbeddedLinuxSoundpack(t *testing.T) { + testenv.IsolateXDG(t) + + tempDir := t.TempDir() + if err := os.Mkdir(filepath.Join(tempDir, "linux"), 0755); err != nil { + t.Fatalf("failed to create same-name directory: %v", err) + } + t.Chdir(tempDir) + + cli := NewCLI() + cfg := &config.Config{ + DefaultSoundpack: "linux", + Enabled: false, + } + + if err := initializeAudioSystem(cli.rootCmd, cli, cfg); err != nil { + t.Fatalf("initializeAudioSystem returned error: %v", err) + } + if cli.soundpackResolver == nil { + t.Fatal("soundpack resolver was not initialized") + } + if got := cli.soundpackResolver.GetType(); got != "json" { + t.Fatalf("expected bare linux to initialize embedded JSON resolver, got type %q", got) + } + if got := cli.soundpackResolver.GetName(); got != "linux-default" { + t.Fatalf("expected embedded linux resolver name %q, got %q", "linux-default", got) + } + + resolved, err := cli.soundpackResolver.ResolveSound("default.wav") + if err != nil { + t.Fatalf("expected embedded linux resolver to resolve default.wav: %v", err) + } + if _, err := os.Stat(resolved); err != nil { + t.Fatalf("resolved default.wav path %q does not exist: %v", resolved, err) + } +} + // TestEmbeddedLinuxSoundpackResolves proves the native-Linux default pack // resolves its default tones out of the box on any host. // diff --git a/internal/cli/embedded_soundpack_discovery_test.go b/internal/cli/embedded_soundpack_discovery_test.go new file mode 100644 index 0000000..9417186 --- /dev/null +++ b/internal/cli/embedded_soundpack_discovery_test.go @@ -0,0 +1,23 @@ +package cli + +import "testing" + +func TestDiscoverEmbeddedSoundpacksIncludesLinux(t *testing.T) { + packs, err := discoverEmbeddedSoundpacks() + if err != nil { + t.Fatalf("discoverEmbeddedSoundpacks returned error: %v", err) + } + + found := map[string]bool{} + for _, pack := range packs { + if pack.Type == "embedded" { + found[pack.Name] = true + } + } + + for _, name := range []string{"windows", "wsl", "darwin", "linux"} { + if !found[name] { + t.Errorf("expected embedded soundpack discovery to include %q", name) + } + } +} diff --git a/internal/cli/soundpack_helpers.go b/internal/cli/soundpack_helpers.go index 6bb54ed..c5d82ef 100644 --- a/internal/cli/soundpack_helpers.go +++ b/internal/cli/soundpack_helpers.go @@ -24,6 +24,27 @@ type soundpackInfo struct { Path string } +var embeddedPlatformSoundpackFiles = []string{"windows.json", "wsl.json", "darwin.json", "linux.json"} + +func embeddedPlatformSoundpackIdentifier(name string) (string, bool) { + if name == "" || strings.ContainsAny(name, `/\`) || filepath.Ext(name) != "" { + return "", false + } + + filename := name + ".json" + for _, embedded := range embeddedPlatformSoundpackFiles { + if filename == embedded { + return "embedded:" + filename, true + } + } + + if _, err := config.GetEmbeddedPlatformSoundpackData(filename); err == nil { + return "embedded:" + filename, true + } + + return "", false +} + // discoverSoundpacks finds all available soundpacks from embedded, XDG, and config sources. // Returns a deduplicated list of soundpack info structs. func discoverSoundpacks() ([]soundpackInfo, error) { @@ -80,12 +101,11 @@ func discoverSoundpacks() ([]soundpackInfo, error) { return packs, nil } -// discoverEmbeddedSoundpacks returns info for the 3 embedded platform packs +// discoverEmbeddedSoundpacks returns info for embedded platform packs. func discoverEmbeddedSoundpacks() ([]soundpackInfo, error) { - platformFiles := []string{"windows.json", "wsl.json", "darwin.json"} var packs []soundpackInfo - for _, file := range platformFiles { + for _, file := range embeddedPlatformSoundpackFiles { data, err := config.GetEmbeddedPlatformSoundpackData(file) if err != nil { slog.Warn("failed to read embedded platform soundpack", "file", file, "error", err) @@ -292,15 +312,14 @@ func countNonEmptyMappings(mappings map[string]string) int { return count } -// ExtractAllSoundKeys reads all 3 embedded platform JSONs and returns the sorted +// ExtractAllSoundKeys reads all embedded platform JSONs and returns the sorted // union of all mapping keys. It uses PeekJSONSoundpackFromBytes (which // applies the size and mappings-count caps but skips path/existence // checks) since we only need the keys. func ExtractAllSoundKeys() ([]string, error) { - platformFiles := []string{"windows.json", "wsl.json", "darwin.json"} keySet := make(map[string]struct{}) - for _, file := range platformFiles { + for _, file := range embeddedPlatformSoundpackFiles { data, err := config.GetEmbeddedPlatformSoundpackData(file) if err != nil { slog.Warn("failed to read embedded platform soundpack", "file", file, "error", err)