// 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") } } }