Files
fenrir/internal/app/diagnostics_test.go

110 lines
3.6 KiB
Go

// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
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)
}
})
}
}