// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "log/slog" "reflect" "testing" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/review" ) func TestReadingSpeechText(t *testing.T) { for _, test := range []struct { name string settings config.ReadingSettings input string want string }{ {"unchanged", config.ReadingSettings{}, "saveFile 123", "saveFile 123"}, {"multicase", config.ReadingSettings{SpeakMultiCaseStringsAsWords: true}, "saveFile and HTTPServer", "save File and HTTPServer"}, {"digits", config.ReadingSettings{SpeakNumbersAsDigits: true}, "Room 123, A12, ٤٥", "Room 1 2 3, A12, ٤ ٥"}, {"both", config.ReadingSettings{SpeakNumbersAsDigits: true, SpeakMultiCaseStringsAsWords: true}, "orderNumber 42", "order Number 4 2"}, } { t.Run(test.name, func(t *testing.T) { if got := readingSpeechText(test.input, test.settings); got != test.want { t.Fatalf("speech text = %q, want %q", got, test.want) } }) } } func TestPronunciationPreservesCharacterReadingAndSpelling(t *testing.T) { entries := []config.PronunciationEntry{{Word: "a", Replacement: "article"}, {Word: "space", Replacement: "blank"}} for _, test := range []struct { name string request speechRequest want []string }{ {"character", speechRequest{Text: "A", Character: true}, []string{"A"}}, {"echo", speechRequest{Text: "a", Character: true, CustomEcho: true}, []string{"a"}}, {"spelling", func() speechRequest { request := speechRequest{Text: "Aa"} set_review_spelling(&request, "Aa", review.FlatSpell) return request }(), []string{"A", "a"}}, {"word", speechRequest{Text: "a"}, []string{"article"}}, {"character name", speechRequest{Text: "space", Character: true}, []string{"blank"}}, } { t.Run(test.name, func(t *testing.T) { speaker := &spellingTestSpeaker{} worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker) worker.textTransform = func(text string) string { return pronunciationSpeechText(text, entries) } if err := worker.deliver(context.Background(), test.request); err != nil { t.Fatal(err) } if !reflect.DeepEqual(speaker.parts, test.want) { t.Fatalf("delivered %q, want %q", speaker.parts, test.want) } }) } } func TestSpeechHistoryReplayDoesNotApplyPronunciationTwice(t *testing.T) { speaker := &spellingTestSpeaker{} worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker) worker.history = newSpeechHistory(10) worker.textTransform = func(text string) string { return pronunciationSpeechText(text, []config.PronunciationEntry{ {Word: "SQL", Replacement: "sequel"}, {Word: "sequel", Replacement: "next"}, }) } if err := worker.deliver(context.Background(), speechRequest{Text: "SQL"}); err != nil { t.Fatal(err) } recorded := worker.history.snapshot()[0] if err := worker.deliver(context.Background(), speechRequest{Text: recorded, Role: "table cell", ExcludeFromHistory: true}); err != nil { t.Fatal(err) } if !reflect.DeepEqual(speaker.parts, []string{"sequel", "sequel"}) { t.Fatalf("history replay changed recorded speech: %q", speaker.parts) } } func TestPronunciationSpeechTextReplacesWholeWordsOnce(t *testing.T) { entries := []config.PronunciationEntry{{Word: "SQL", Replacement: "sequel"}, {Word: "sequel", Replacement: "next"}, {Word: "café", Replacement: "coffee"}} input := "SQL, sql; SQLite café!" if got := pronunciationSpeechText(input, entries); got != "sequel, sequel; SQLite coffee!" { t.Fatalf("pronunciation = %q", got) } }