// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "os" "os/exec" "path/filepath" "strings" "testing" "time" "git.stormux.org/storm/fenrir/internal/control" "git.stormux.org/storm/fenrir/internal/interaction" ) func TestApplicationAliasesShellCheck(t *testing.T) { path, err := exec.LookPath("shellcheck") if err != nil { t.Skip("shellcheck unavailable") } for _, id := range []string{"networkmanager", "ninjam", "rogue", "codex"} { process := exec.Command(path, "-s", "bash", "-") process.Stdin = strings.NewReader(application_alias(id, "") + "\n") if output, err := process.CombinedOutput(); err != nil { t.Fatalf("%s: %v: %s", id, err, output) } } } func TestApplicationAliasThroughPrivateSocket(t *testing.T) { socatPath, err := exec.LookPath("socat") if err != nil { t.Skip("socat unavailable") } directory := t.TempDir() socketPath := filepath.Join(directory, "reader.sock") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() server, err := control.Open(ctx, control.Options{SocketFile: socketPath, RegistryDirectory: directory}) if err != nil { t.Fatal(err) } defer server.Close() if err := os.Symlink(socatPath, filepath.Join(directory, "socat")); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(directory, "cninjam"), []byte("#!/bin/sh\necho application-launched\n"), 0700); err != nil { t.Fatal(err) } a, _ := persistence_application(t, true) override_read(t, a, override_screen(1, "[Username@host directory] $")) process := exec.CommandContext(ctx, "/bin/bash", "--noprofile", "--norc", "-c", "shopt -s expand_aliases\n"+application_alias("ninjam", socketPath)+"\ncninjam\n") process.Env = append(os.Environ(), "PATH="+directory) completed := make(chan struct{}) var output []byte var processError error go func() { output, processError = process.CombinedOutput() close(completed) }() select { case request := <-server.Requests(): if request.Text != "command application ninjam" { t.Fatal(request.Text) } a.remote_request(request) case <-ctx.Done(): t.Fatal("alias did not send selection request") } <-completed if processError != nil || string(output) != "application-launched\n" || a.overrides[1] == nil { t.Fatalf("launch failed: %v, %s", processError, output) } override_read(t, a, override_screen(1, "ninjam")) if a.overrideCurrent == nil || a.overrideCurrent.id != "ninjam" { t.Fatal("pre-launch selection did not activate when application appeared") } } func TestApplicationAliasLaunchesWithArgumentsDespiteReaderFailure(t *testing.T) { for _, scenario := range []string{"success", "rejected", "failed", "missing"} { t.Run(scenario, func(t *testing.T) { directory := t.TempDir() requestPath := filepath.Join(directory, "request") argsPath := filepath.Join(directory, "args") program := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$ALIAS_ARGS\"\nexit 7\n" if err := os.WriteFile(filepath.Join(directory, "cninjam"), []byte(program), 0700); err != nil { t.Fatal(err) } if scenario != "missing" { client := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$ALIAS_REQUEST\"\n/bin/cat >> \"$ALIAS_REQUEST\"\n" if scenario == "rejected" { client += "echo 'Command remote control is disabled'\n" } if scenario == "failed" { client += "exit 1\n" } if err := os.WriteFile(filepath.Join(directory, "socat"), []byte(client), 0700); err != nil { t.Fatal(err) } } socketPath := "/tmp/example socket'$(false).sock" script := "set -euo pipefail\nshopt -s expand_aliases\n" + application_alias("ninjam", socketPath) + "\ncninjam 'argument with spaces' '*.wav'\n" process := exec.Command("/bin/bash", "--noprofile", "--norc", "-c", script) process.Env = append(os.Environ(), "PATH="+directory, "ALIAS_ARGS="+argsPath, "ALIAS_REQUEST="+requestPath) output, err := process.CombinedOutput() if failure, ok := err.(*exec.ExitError); !ok || failure.ExitCode() != 7 { t.Fatalf("application status lost: %v, %s", err, output) } args, err := os.ReadFile(argsPath) if err != nil || string(args) != "argument with spaces\n*.wav\n" { t.Fatal("arguments lost", string(args), err) } if scenario != "missing" { request, err := os.ReadFile(requestPath) if err != nil || string(request) != "-T\n2\n-\nUNIX-CLIENT:"+socketPath+"\ncommand application ninjam\n" { t.Fatal("socket or request changed by shell", string(request), err) } } if scenario == "rejected" && !strings.Contains(string(output), "Command remote control is disabled") { t.Fatal("reader error hidden", string(output)) } }) } } func TestApplicationsMenuCopiesAliasWithoutSelecting(t *testing.T) { a, voice := persistence_application(t, true) override_read(t, a, override_screen(1, "ninjam")) a.open_applications() a.ui.handle(interaction.Key{Name: "tab"}) a.ui.handle(interaction.Key{Name: "enter"}) if got := voice.messages[len(voice.messages)-1]; got != "Default has no application alias" { t.Fatal(got) } a.ui.handle(interaction.Key{Name: "tab", Shift: true}) // Use the list's prefix search, then Tab to the copy button. a.ui.handle(interaction.Key{Text: "n"}) a.ui.handle(interaction.Key{Text: "i"}) a.ui.handle(interaction.Key{Name: "tab"}) a.ui.handle(interaction.Key{Name: "enter"}) if !a.ui.active || len(a.overrides) != 0 { t.Fatal("copy closed menu or selected an override") } text, ok := a.clipboards.history.Current() if !ok || text != application_alias("ninjam", "") { t.Fatal("wrong clipboard contents", text) } a.ui.handle(interaction.Key{Name: "escape"}) }