Files
fenrir/internal/app/screen_policy_test.go

185 lines
6.4 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"os"
"reflect"
"strings"
"testing"
"git.stormux.org/storm/fenrir/internal/config"
"git.stormux.org/storm/fenrir/internal/control"
"git.stormux.org/storm/fenrir/internal/input"
"git.stormux.org/storm/fenrir/internal/screen"
)
func TestLoginPromptStartupAndDelayedActivation(t *testing.T) {
for _, terminal := range []int{1, 2} {
// tty1 already has its boot login prompt. tty2's login program
// opens the console later, without a keyboard or screen event.
prompt := screen.Snapshot{Terminal: terminal, Active: true, Rows: 1, Cols: 7, Cells: []rune("login: ")}
source := &fakeSource{snapshot: prompt}
if terminal == 2 {
source.snapshot = screen.Snapshot{Terminal: terminal}
}
voice := &fakeVoice{}
var modes []bool
keys := &fakeKeys{poll: func(active bool, _ func(input.Decision)) error {
modes = append(modes, active)
return nil
}}
a, err := new_application(context.Background(), source, keys, voice, RunOptions{Settings: config.Defaults()})
if err != nil {
t.Fatal(err)
}
t.Cleanup(a.ui.close)
if err := a.read_screen(); err != nil {
t.Fatal(err)
}
if err := a.poll_input(true); err != nil {
t.Fatal(err)
}
if terminal == 2 {
if a.lastActive || len(voice.messages) != 0 {
t.Fatal("unused console was captured or read", a.lastActive, voice.messages)
}
source.snapshot = prompt
if err := a.poll_input(false); err != nil {
t.Fatal(err)
}
}
wantModes := []bool{true}
if terminal == 2 {
wantModes = []bool{false, true}
}
if !a.lastActive || !reflect.DeepEqual(modes, wantModes) || !strings.Contains(strings.Join(voice.messages, "|"), "login:") {
t.Fatal("login prompt did not resume input and speech", terminal, modes, voice.messages)
}
}
}
// Model the console's policy boundary independently of the shared application.
type policySource struct {
fakeSource
ignoredVTs []int
}
func (s *policySource) Configure_ignored_vts(numbers []int) {
s.ignoredVTs = append([]int(nil), numbers...)
}
func (s *policySource) Read() (screen.Snapshot, error) {
for _, number := range s.ignoredVTs {
if number == s.snapshot.Terminal {
return screen.Snapshot{Terminal: number}, nil
}
}
return s.fakeSource.Read()
}
func (s *policySource) State() (int, bool, bool, error) {
snapshot, err := s.Read()
return snapshot.Terminal, snapshot.Active, snapshot.NumLock, err
}
func TestRemoteIgnoredVTListsAreAtomic(t *testing.T) {
base := config.Defaults()
base.Screen.IgnoredVTs = []int{2, 3}
for _, text := range []string{
"SET screen#ignore_screen=7, 8,10;speech#enabled=False",
"SET screen#ignore_screen=7, 8,10,speech#enabled=False",
} {
updated, err := remote_settings(base, text)
if err != nil || !reflect.DeepEqual(updated.Screen.IgnoredVTs, []int{7, 8, 10}) || updated.Presentation.Speech.Enabled {
t.Fatal("lost list or following assignment", updated, err)
}
}
for _, value := range []string{"0", "64", "-1", "7,7", "7,", "7,,8", "tty7", "7;unknown#setting=True"} {
failed, err := remote_settings(base, "SET speech#enabled=False;screen#ignore_screen="+value)
if err == nil || !reflect.DeepEqual(failed, base) || !reflect.DeepEqual(base.Screen.IgnoredVTs, []int{2, 3}) {
t.Fatal("invalid list partially applied", value, failed, base, err)
}
}
updated, err := remote_settings(base, "SET screen#ignore_screen=")
if err != nil || len(updated.Screen.IgnoredVTs) != 0 {
t.Fatal("empty list did not clear policy", updated.Screen, err)
}
}
func TestIgnoredVTStartupRemoteReleaseRecoveryAndTerminalIsolation(t *testing.T) {
for _, terminalMode := range []bool{false, true} {
settings := config.Defaults()
settings.Screen.IgnoredVTs = []int{2, 7, 8, 10}
source := &policySource{fakeSource: fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 1, Cols: 5, Cells: []rune("alpha")}}}
voice := &fakeVoice{}
var modes []bool
keys := &fakeKeys{poll: func(active bool, _ func(input.Decision)) error {
modes = append(modes, active)
return nil
}}
a, err := new_application(context.Background(), source, keys, voice, RunOptions{Settings: settings, TerminalMode: terminalMode})
if err != nil {
t.Fatal(err)
}
t.Cleanup(a.ui.close)
if err := a.read_screen(); err != nil {
t.Fatal(err)
}
if err := a.poll_input(true); err != nil {
t.Fatal(err)
}
if a.lastActive != terminalMode || (!terminalMode && len(voice.messages) != 0) {
t.Fatal("wrong startup policy", terminalMode, a.lastActive, voice.messages)
}
request := func(uid int, text string) string {
t.Helper()
replies := make(chan string, 1)
a.remote_request(control.Request{UID: uid, Text: "SETTING " + text, Reply: replies})
return <-replies
}
if reply := request(os.Geteuid()+1, "SET screen#ignore_screen="); reply == "" || !reflect.DeepEqual(a.settings.live.Screen, settings.Screen) {
t.Fatal("unauthorized policy change", reply, a.settings.live.Screen)
}
if reply := request(os.Geteuid(), "SET screen#ignore_screen="); reply != "" {
t.Fatal(reply)
}
if err := a.poll_input(true); err != nil || !a.lastActive {
t.Fatal("did not recover after remote clear", err)
}
a.ui.open_tutorial(nil, nil, nil)
interrupts := voice.interrupts
if reply := request(os.Geteuid(), "SET screen#ignore_screen=2,7,8,10"); reply != "" {
t.Fatal(reply)
}
if err := a.poll_input(false); err != nil {
t.Fatal(err)
}
if !terminalMode && (a.lastActive || a.ui.active || voice.interrupts <= interrupts || !reflect.DeepEqual(keys.captures, []bool{true, false})) {
t.Fatal("handoff retained modal capture or speech", a.lastActive, a.ui.active, keys.captures, voice.interrupts)
}
if terminalMode && (!a.lastActive || !a.ui.active) {
t.Fatal("console policy affected X11 session")
}
source.snapshot.Terminal = 3
if err := a.poll_input(false); err != nil || !a.lastActive {
t.Fatal("allowed VT did not resume", err)
}
if !terminalMode && !reflect.DeepEqual(modes, []bool{false, true, false, true}) {
t.Fatal("wrong handoff order", modes)
}
// Applying a saved draft uses the same policy; quick-save must retain it.
saved := settings
saved.Screen.IgnoredVTs = []int{3}
a.apply_saved_settings(saved, false)
if err := a.poll_input(false); err != nil || a.lastActive != terminalMode {
t.Fatal("saved policy was not applied", err, a.lastActive)
}
if !reflect.DeepEqual(a.settings.saveDraft.Screen.IgnoredVTs, []int{3}) {
t.Fatal("lost saved policy")
}
}
}