// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "errors" "log/slog" "reflect" "strings" "testing" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/review" ) func TestFlatReviewSpellingPresentation(t *testing.T) { for _, tc := range []struct { command input.Command text string parts []string character bool }{ {input.ReviewCurrentWord, "book", nil, false}, {input.ReviewCurrentCharacter, "b", nil, true}, {input.ReviewPhoneticCharacter, "B", []string{"bravo"}, false}, {input.ReviewSpellWord, "Book", []string{"B", "o", "o", "k"}, true}, {input.ReviewPhoneticWord, "Book", []string{"bravo", "oscar", "oscar", "kilo"}, false}, {input.ReviewSpellWord, "猫1!", []string{"猫", "1", "!"}, true}, {input.ReviewPhoneticWord, "猫1!", []string{"猫", "1", "!"}, false}, {input.ReviewPhoneticCharacter, " ", nil, true}, {input.ReviewPhoneticWord, "", nil, false}, } { queue := &fakeSpeechQueue{requests: make(chan speechRequest, 1), spoken: make(chan string, 1)} unit, step := flatReviewCommand(tc.command) result := review.Result{FlatUnit: unit, FlatStep: step, FlatSpeech: flat_review_speech(tc.command), FlatPosition: review.FlatPosition{Text: tc.text}} presentFlatReviewResult(slog.New(slog.DiscardHandler), queue, result) request := <-queue.requests if !reflect.DeepEqual(request.Spelling, tc.parts) || request.Character != tc.character { t.Fatalf("%s %q request=%#v", tc.command, tc.text, request) } } } type spellingTestSpeaker struct { parts []string characters []string after func() } func (*spellingTestSpeaker) Connect(context.Context) error { return nil } func (*spellingTestSpeaker) Close(context.Context) error { return nil } func (*spellingTestSpeaker) Cancel(context.Context) error { return nil } func (speaker *spellingTestSpeaker) Speak(_ context.Context, text string) error { speaker.parts = append(speaker.parts, text) if speaker.after != nil { speaker.after() } return nil } func (speaker *spellingTestSpeaker) SpeakCharacter(ctx context.Context, text string) error { speaker.characters = append(speaker.characters, text) return speaker.Speak(ctx, text) } func TestReviewSpellingDeliveryAndInterruption(t *testing.T) { for _, style := range []review.FlatSpeech{review.FlatSpell, review.FlatPhonetic} { speaker := &spellingTestSpeaker{} worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker) request := speechRequest{Text: "book", Cause: "flat-review"} set_review_spelling(&request, strings.Repeat("book", 20), style) if err := worker.deliver(context.Background(), request); err != nil { t.Fatal(err) } if len(speaker.parts) != 80 { t.Fatalf("long spelling lost letters: %d", len(speaker.parts)) } wantCharacters := 0 if style == review.FlatSpell { wantCharacters = 80 } if len(speaker.characters) != wantCharacters { t.Fatalf("character-mode delivery=%d", len(speaker.characters)) } worker.running = true // Exercise the real interruption generation barrier. speaker.parts = nil speaker.after = worker.InterruptForUser if err := worker.deliver(context.Background(), request); !errors.Is(err, context.Canceled) { t.Fatalf("interrupt=%v", err) } if len(speaker.parts) != 1 { t.Fatalf("spelling continued after input: %v", speaker.parts) } } }