// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . package app import ( "bytes" "context" "encoding/json" "log/slog" "strings" "testing" "time" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/input" "git.stormux.org/storm/fenrir/internal/screen" ) func TestPresentationDiagnosticsExplainSuppressionWithoutText(t *testing.T) { for _, testCase := range []struct { name string action command.ID muted bool silent bool }{ {name: "ordinary"}, {name: "muted", action: command.Mute, muted: true}, {name: "temporary silence", action: command.TempDisableSpeech, silent: true}, } { t.Run(testCase.name, func(t *testing.T) { var log bytes.Buffer previousLogger := slog.Default() slog.SetDefault(slog.New(slog.NewJSONHandler(&log, &slog.HandlerOptions{Level: slog.LevelDebug}))) defer slog.SetDefault(previousLogger) source := &fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 1, Cols: 7, Cells: []rune("PRIVATE")}, events: make(chan error, 1)} keys := &fakeKeys{events: make(chan error, 1)} reads, polls := 0, 0 source.afterRead = func() { reads++ if reads == 2 { keys.events <- nil } } keys.poll = func(_ bool, handle func(input.Decision)) error { polls++ if polls == 1 { handle(input.Decision{Command: testCase.action, Interrupt: true, Keypress: true}) source.snapshot.Cells = []rune("SECRET!") source.events <- nil } else { handle(input.Decision{Command: command.Quit}) } return nil } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := run(ctx, source, keys, &fakeVoice{}, nil); err != nil || reads != 2 || polls != 2 { t.Fatal("fixture did not process the screen update", err, reads, polls) } if strings.Contains(log.String(), "PRIVATE") || strings.Contains(log.String(), "SECRET") { t.Fatal("diagnostics exposed screen or speech text") } var presentation struct { Msg string Messages int Muted bool TemporarySilence bool `json:"temporary_silence"` UIActive bool `json:"ui_active"` } inputLogged := false for _, line := range bytes.Split(bytes.TrimSpace(log.Bytes()), []byte("\n")) { var record struct { Msg string Interrupt bool Keypress bool } if err := json.Unmarshal(line, &record); err != nil { t.Fatal(err) } if record.Msg == "reader presentation" { if err := json.Unmarshal(line, &presentation); err != nil { t.Fatal(err) } } if record.Msg == "input decision" && record.Interrupt && record.Keypress { inputLogged = true } } if !inputLogged || presentation.Messages != 1 || presentation.Muted != testCase.muted || presentation.TemporarySilence != testCase.silent || presentation.UIActive { t.Fatal("missing or incorrect input/presentation state", inputLogged, presentation) } }) } }