package main
import (
"context"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
const home = "/home/op"
// fake is a machine with these files under a temporary root; a value starting with "->" is a
// symbolic link to the rest, and one starting with "#!" is an executable.
func fake(t *testing.T, files map[string]string) *Machine {
t.Helper()
root := t.TempDir()
for p, c := range files {
full := filepath.Join(root, p)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
switch {
case strings.HasPrefix(c, "->"):
if err := os.Symlink(strings.TrimPrefix(c, "->"), full); err != nil {
t.Fatal(err)
}
case c == "
":
if err := os.MkdirAll(full, 0o755); err != nil {
t.Fatal(err)
}
default:
mode := os.FileMode(0o644)
if strings.HasPrefix(c, "#!") {
mode = 0o755
}
if err := os.WriteFile(full, []byte(c), mode); err != nil {
t.Fatal(err)
}
}
}
return &Machine{Root: root, Home: home, UID: os.Getuid(), Desktop: "i3", Timeout: time.Second,
Run: func(context.Context, []string, string, ...string) (string, error) { return "", nil }}
}
func app(name, exec, mime string) string {
s := "[Desktop Entry]\nType=Application\nName=" + name + "\nExec=" + exec + "\n"
if mime != "" {
s += "MimeType=" + mime + "\n"
}
return s
}
// The desktop's folders as found on 2026-10-05, in the module's file with that machine's settings.
func desktopUserDirs(t *testing.T) string {
raw, err := os.ReadFile(filepath.Join("..", "..", "config", "user-dirs.dirs"))
if err != nil {
t.Fatal(err)
}
r := strings.NewReplacer("${setting:desktop-dir}", "$HOME/", "${setting:publicshare-dir}", "$HOME/", "${setting:videos-dir}", "$HOME/")
return r.Replace(string(raw))
}
func TestTheFoldersAreReadAsXdgUserDirResolvesThem(t *testing.T) {
conf, _ := os.ReadFile(filepath.Join("..", "..", "config", "user-dirs.conf"))
m := fake(t, map[string]string{
home + "/.config/user-dirs.dirs": desktopUserDirs(t),
home + "/.config/user-dirs.conf": string(conf),
home + "/.config/user-dirs.locale": "en_US\n",
"/etc/xdg/user-dirs.conf": "enabled=True\n",
"/etc/xdg/user-dirs.defaults": "# c\nDESKTOP=Desktop\nDOWNLOAD=Downloads\nPROJECTS=Projects\nNEWTHING=New\n",
home + "/Desktop": "a file where the folder would be\n",
home + "/Downloads": "",
home + "/projects": "",
})
a := m.UserDirs()
if !a.Meshs || a.Locale != "en_US" || a.Update.Enabled || a.Update.DecidedBy != "~/.config/user-dirs.conf" {
t.Fatalf("%+v", a)
}
got := map[string]UserDir{}
for _, d := range a.Dirs {
got[d.Name] = d
}
if len(a.Dirs) != 9 {
t.Fatalf("%d folders: %+v", len(a.Dirs), a.Dirs)
}
if d := got["DESKTOP"]; !d.Home || d.Path != "~" || d.Default != "~/Desktop" {
t.Errorf("DESKTOP: %+v", d)
}
if d := got["DOWNLOAD"]; d.Home || !d.Exists || d.Path != "~/Downloads" {
t.Errorf("DOWNLOAD: %+v", d)
}
if d := got["PROJECTS"]; d.Path != "~/projects" || !d.Exists || d.Default != "~/Projects" {
t.Errorf("PROJECTS: %+v", d)
}
if d := got["MUSIC"]; d.Exists || d.Kind != "absent" {
t.Errorf("MUSIC: %+v", d)
}
if !reflect.DeepEqual(a.Missing, []string{"NEWTHING"}) {
t.Errorf("defaults without a line: %v", a.Missing)
}
}
func TestAnUpdateLeftEnabledIsSaid(t *testing.T) {
m := fake(t, map[string]string{"/etc/xdg/user-dirs.conf": "# c\nenabled=True\nfilename_encoding=UTF-8\n"})
a := m.UserDirs()
if !a.Update.Enabled || a.Update.DecidedBy != "/etc/xdg/user-dirs.conf" || !strings.Contains(a.Update.Note, "rewrites the file") {
t.Fatalf("%+v", a.Update)
}
}
func TestATypeIsAMimeTypeOrAScheme(t *testing.T) {
for in, want := range map[string]string{
"https": "x-scheme-handler/https", "mailto:": "x-scheme-handler/mailto", "http://": "x-scheme-handler/http",
"text/html": "text/html", "x-scheme-handler/tg": "x-scheme-handler/tg", "application/xhtml+xml": "application/xhtml+xml",
} {
if got, err := Normalise(in); err != nil || got != want {
t.Errorf("%s: %s %v", in, got, err)
}
}
for _, bad := range []string{"", "text/html; rm -rf", "../x", "Text Html"} {
if _, err := Normalise(bad); err == nil {
t.Errorf("%q accepted", bad)
}
}
}
func TestTheProgramIsFoundBehindLaunchers(t *testing.T) {
// Exec lines found on the workstations.
for exec, want := range map[string]string{
`env APPIMAGELAUNCHER_DISABLE=1 DESKTOPINTEGRATION=0 /usr/bin/Plexamp.AppImage --disable-seccomp-filter-sandbox`: "/usr/bin/Plexamp.AppImage",
`sh -c "/opt/forticlient/gui/FortiClient >/dev/null 2>&1"`: "/opt/forticlient/gui/FortiClient",
`zsh -c 'dockly'`: "dockly",
`"/opt/claude-code/bin/claude" --handle-uri %u`: "/opt/claude-code/bin/claude",
`env LIBGL_ALWAYS_SOFTWARE=1 Telegram -- %U`: "Telegram",
`/usr/bin/slack --gtk-version=3 -s %U`: "/usr/bin/slack",
`"/path with space/Fleet" %u`: "/path with space/Fleet",
`steam steam://rungameid/427520`: "steam",
`xterm -fa monospace -fs 11 -e glow -p %f`: "xterm",
``: "",
} {
if got := Program(exec); got != want {
t.Errorf("%s: %q, want %q", exec, got, want)
}
}
}
func TestADuplicateGroupOrKeyIsReportedNotMerged(t *testing.T) {
k := ParseKeyFile("# >>> mesh >>>\n[Default Applications]\nx=a.desktop\n# <<<\n\n[Default Applications]\nx=b.desktop\ny=c\n[Added Associations]\nz=d;\nstray\n")
if !reflect.DeepEqual(k.DupGroups, []string{"Default Applications"}) || !reflect.DeepEqual(k.DupKeys, []string{"Default Applications: x"}) {
t.Fatalf("%+v", k)
}
if k.Groups["Default Applications"]["x"] != "a.desktop" || k.Groups["Default Applications"]["y"] != "c" || len(k.Unparsable) != 1 {
t.Fatalf("%+v", k)
}
}
// A machine with the mesh's list, the person's list, and some applications.
func mimeMachine(t *testing.T, extra map[string]string) *Machine {
mesh, err := os.ReadFile(filepath.Join("..", "..", "config", "mimeapps.list"))
if err != nil {
t.Fatal(err)
}
files := map[string]string{
MeshList: string(mesh),
home + "/.config/mimeapps.list": "[Default Applications]\ntext/plain=code-oss.desktop;\ninode/directory=xplr.desktop\nx-scheme-handler/https=gone.desktop;chromium.desktop;\n",
"/usr/share/applications/firefox.desktop": app("Firefox", "/usr/lib/firefox/firefox %u", "text/html;x-scheme-handler/https;"),
"/usr/share/applications/chromium.desktop": app("Chromium", "/usr/bin/chromium %U", "x-scheme-handler/https;"),
"/usr/share/applications/thunar.desktop": app("Thunar", "thunar %F", "inode/directory;"),
home + "/.local/share/applications/xplr.desktop": app("xplr", "xplr", "inode/directory;"),
home + "/.local/share/applications/fleet.desktop": app("Fleet", `"/home/op/fleet/bin/Fleet" %u`, "text/plain;"),
"/usr/lib/firefox/firefox": "#!/bin/sh\n",
"/usr/bin/chromium": "#!/bin/sh\n",
"/usr/bin/thunar": "#!/bin/sh\n",
}
for k, v := range extra {
files[k] = v
}
return fake(t, files)
}
func TestTheDefaultIsDecidedInTheSpecificationsOrder(t *testing.T) {
m := mimeMachine(t, nil)
// The person's list names an application that is gone, then one that is installed.
if d := m.Decide("x-scheme-handler/https"); d.ID != "chromium.desktop" || d.File != "~/.config/mimeapps.list" ||
!reflect.DeepEqual(d.Skipped, []string{"gone.desktop (~/.config/mimeapps.list): not installed"}) {
t.Fatalf("%+v", d)
}
// The person's choice wins over the mesh's.
if d := m.Decide("inode/directory"); d.ID != "xplr.desktop" {
t.Fatalf("%+v", d)
}
// What the person has not chosen, the mesh's list answers.
if d := m.Decide("text/html"); d.ID != "firefox.desktop" || d.File != MeshList {
t.Fatalf("%+v", d)
}
// Nothing installed is named: no list decides.
if d := m.Decide("text/plain"); d.ID != "" || len(d.Skipped) != 1 {
t.Fatalf("%+v", d)
}
// The desktop's own list comes first of all.
m2 := mimeMachine(t, map[string]string{home + "/.config/i3-mimeapps.list": "[Default Applications]\ninode/directory=thunar.desktop\n"})
if d := m2.Decide("inode/directory"); d.ID != "thunar.desktop" || d.File != "~/.config/i3-mimeapps.list" {
t.Fatalf("%+v", d)
}
}
// recorded answers xdg-mime and gio as they answered on the laptop on 2026-10-05.
func recorded(calls *[]string) Runner {
return func(_ context.Context, env []string, name string, args ...string) (string, error) {
*calls = append(*calls, name+" "+strings.Join(args, " "))
key := name + " " + strings.Join(args, " ")
switch key {
case "xdg-mime query default audio/mpeg":
return "audacity.desktop\n", nil
case "gio mime audio/mpeg":
return "Default application for “audio/mpeg”: mpv.desktop\nRegistered applications:\n\tmpv.desktop\n\taudacity.desktop\nRecommended applications:\n\tmpv.desktop\n\taudacity.desktop\n", nil
case "xdg-mime query default x-scheme-handler/https":
return "chromium.desktop\n", nil
case "gio mime x-scheme-handler/https":
return "Default application for “x-scheme-handler/https”: chromium.desktop\nRegistered applications:\n\tchromium.desktop\n", nil
case "xdg-mime query default x-scheme-handler/nothing-here":
return "", nil
case "gio mime x-scheme-handler/nothing-here":
return "No default applications for “x-scheme-handler/nothing-here”\n", nil
}
return "", nil
}
}
func TestTwoReadersThatDisagreeAreSaid(t *testing.T) {
var calls []string
m := mimeMachine(t, map[string]string{
"/usr/share/applications/audacity.desktop": app("Audacity", "audacity %F", "audio/mpeg;"),
"/usr/share/applications/mpv.desktop": app("mpv", "mpv -- %U", "audio/mpeg;video/mp4;"),
"/usr/bin/audacity": "#!/bin/sh\n",
})
m.Run = recorded(&calls)
h := m.Handle("audio/mpeg")
if h.XdgOpen != "audacity.desktop" || h.Gio != "mpv.desktop" || !h.Disagree || h.App == nil || !h.App.Found {
t.Fatalf("%+v", h)
}
if !strings.Contains(h.Decision.File, "no list names") {
t.Fatalf("%+v", h.Decision)
}
none := m.Handle("x-scheme-handler/nothing-here")
if none.XdgOpen != "" || none.Gio != "" || none.Problem != "nothing opens it" {
t.Fatalf("%+v", none)
}
}
func TestTheEnvironmentGivenIsTheAccountsXdgOnly(t *testing.T) {
var env []string
m := mimeMachine(t, nil)
m.Run = func(_ context.Context, e []string, _ string, _ ...string) (string, error) { env = e; return "", nil }
m.Handle("text/html")
joined := strings.Join(env, "\n")
for _, want := range []string{"HOME=" + home, "XDG_CONFIG_HOME=" + home + "/.config", "XDG_CURRENT_DESKTOP=i3", "LANG=C.UTF-8"} {
if !strings.Contains(joined, want) {
t.Errorf("lacks %s: %v", want, env)
}
}
if len(env) != 7 {
t.Errorf("only the seven XDG words are passed on: %v", env)
}
}
func TestSettingADefaultWritesThePersonsListThroughXdgMime(t *testing.T) {
var calls []string
m := mimeMachine(t, nil)
m.Run = recorded(&calls)
a, err := m.SetDefault("x-scheme-handler/https", "chromium.desktop")
if err != nil {
t.Fatal(err)
}
if !contains(calls, "xdg-mime default chromium.desktop x-scheme-handler/https") {
t.Fatalf("%v", calls)
}
if a["written_to"] != "~/.config/mimeapps.list" || a["after"] != "chromium.desktop" || a["problem"] != nil {
t.Fatalf("%v", a)
}
for _, refused := range []string{"gone.desktop", "../../etc/passwd", "firefox", "x.desktop; rm"} {
calls = nil
if _, err := m.SetDefault("text/html", refused); err == nil || len(calls) != 0 {
t.Errorf("%s: %v %v", refused, err, calls)
}
}
}
func TestDeadDefaultsBrokenHandlersAndListsAreFound(t *testing.T) {
m := mimeMachine(t, map[string]string{
home + "/.local/share/applications/mimeapps.list": "->/home/op/.retired/dotfiles/mimeapps.list",
home + "/.local/share/applications/defaults.list": "->/home/op/.retired/dotfiles/mimeapps.list",
home + "/.local/share/applications/fleet-old.desktop": app("Fleet", "/home/op/gone/Fleet %u", "x-scheme-handler/fleet;"),
})
dead := m.deadDefaults()
var ids []string
for _, d := range dead {
// The fake installs none of the mesh's list's applications but firefox and thunar.
if d.File == "~/.config/mimeapps.list" {
ids = append(ids, d.Type+"="+d.ID)
}
}
if !reflect.DeepEqual(ids, []string{"text/plain=code-oss.desktop", "x-scheme-handler/https=gone.desktop"}) {
t.Fatalf("%v", ids)
}
var broken []string
for _, e := range m.brokenHandlers() {
broken = append(broken, e.ID)
}
// xplr's program is not installed in the fake, nor fleet's.
if !reflect.DeepEqual(broken, []string{"fleet-old.desktop", "fleet.desktop", "xplr.desktop"}) {
t.Fatalf("%v", broken)
}
c := m.Check("i3")
checks := map[string]Check{}
for _, x := range c["checks"].([]Check) {
checks[x.Name] = x
}
if s := checks["the mimeapps lists are sane"]; s.OK || !strings.Contains(s.Detail, "a link to nothing") || !strings.Contains(s.Detail, "defaults.list") {
t.Fatalf("%+v", s)
}
if s := checks["the mesh's default applications are in place"]; !s.OK {
t.Fatalf("%+v", s)
}
}
func TestAMeshBlockInThePersonsListIsWhatTheCheckRefuses(t *testing.T) {
// Why the module does not write a block into ~/.config/mimeapps.list: GLib reads the last of two
// groups, xdg-mime the first, and either rewrites the file around the block.
m := mimeMachine(t, map[string]string{
home + "/.config/mimeapps.list": "# >>> mesh >>>\n[Default Applications]\ntext/html=firefox.desktop\n# <<< mesh <<<\n[Default Applications]\ntext/html=chromium.desktop\n",
})
c := m.Check("i3")
for _, x := range c["checks"].([]Check) {
if x.Name == "the mimeapps lists are sane" && (x.OK || !strings.Contains(x.Detail, "written twice")) {
t.Fatalf("%+v", x)
}
}
}
func autostartMachine(t *testing.T) *Machine {
return fake(t, map[string]string{
home + "/.config/autostart/slack.desktop": app("Slack", "/usr/bin/slack -s %U", "") + "Hidden=false\n",
home + "/.config/autostart/blueman.desktop": app("Blueman", "blueman-applet", "") + "Hidden=true\n",
home + "/.config/autostart/nextcloud.desktop": app("Nextcloud", `"/usr/bin/nextcloud" --background`, "") + "X-GNOME-Autostart-enabled=false\n",
"/etc/xdg/autostart/blueman.desktop": app("Blueman", "blueman-applet", ""),
"/etc/xdg/autostart/picom.desktop": app("picom", "picom", "") + "TryExec=picom\n",
"/etc/xdg/autostart/nm-applet.desktop": app("nm", "nm-applet", "") + "NotShowIn=KDE;GNOME;\n",
"/etc/xdg/autostart/gnome-keyring.desktop": app("keyring", "/usr/bin/gnome-keyring-daemon --start", "") + "OnlyShowIn=GNOME;Unity;\n",
"/etc/xdg/autostart/geoclue.desktop": app("geoclue", "/usr/lib/geoclue-2.0/demos/agent", "") + "NotShowIn=i3;\n",
"/etc/xdg/autostart/print.desktop": app("print", "system-config-printer-applet", "") + "TryExec=system-config-printer-applet\n",
"/etc/xdg/autostart/polychromatic.desktop": app("poly", "polychromatic-helper --autostart", ""),
"/etc/xdg/autostart/slack-too.desktop": app("Slack", "/usr/bin/slack", ""),
home + "/.config/i3/config": "exec --no-startup-id dex --autostart --environment i3\nexec --no-startup-id picom\n",
home + "/.config/i3/config.d/90-x.conf": "exec_always --no-startup-id polychromatic-tray-applet\n",
"/usr/bin/slack": "#!/bin/sh\n",
"/usr/bin/picom": "#!/bin/sh\n",
"/usr/bin/nm-applet": "#!/bin/sh\n",
"/usr/bin/nextcloud": "#!/bin/sh\n",
"/usr/bin/polychromatic-helper": "#!/bin/sh\n",
"/proc/101/cmdline": "/usr/lib/slack/slack --type=renderer --enable-crash-reporter",
"/proc/101/comm": "slack\n",
"/proc/102/cmdline": "picom\x00",
"/proc/103/cmdline": "python3\x00/usr/bin/polychromatic-helper\x00--autostart\x00",
"/proc/104/cmdline": "nextcloud-sync-helper\x00",
"/proc/self/cmdline": "not a process\x00",
})
}
func TestAutostartIsDecidedAsDexDecides(t *testing.T) {
m := autostartMachine(t)
got := map[string]Autostart{}
for _, a := range m.Autostarts("i3") {
got[a.Scope+"/"+a.ID] = a
}
want := map[string]bool{
"user/slack.desktop": true, "user/blueman.desktop": false, "user/nextcloud.desktop": true,
"system/blueman.desktop": false, "system/picom.desktop": true, "system/nm-applet.desktop": true,
"system/gnome-keyring.desktop": false, "system/geoclue.desktop": false, "system/print.desktop": false,
"system/polychromatic.desktop": true, "system/slack-too.desktop": true,
}
if len(got) != len(want) {
t.Fatalf("%d entries: %v", len(got), got)
}
for k, starts := range want {
if got[k].DexStarts != starts {
t.Errorf("%s: dex starts %v (%s)", k, got[k].DexStarts, got[k].Why)
}
}
if a := got["system/blueman.desktop"]; a.OverriddenBy != "~/.config/autostart/blueman.desktop" || got["user/blueman.desktop"].Overrides != "/etc/xdg/autostart/blueman.desktop" {
t.Errorf("the user's entry hides the system's of the same name: %+v", a)
}
if a := got["user/nextcloud.desktop"]; !strings.Contains(a.Why, "does not stop dex") {
t.Errorf("%s", a.Why)
}
if a := got["system/print.desktop"]; !strings.Contains(a.Why, "TryExec") {
t.Errorf("%s", a.Why)
}
if a := got["system/picom.desktop"]; a.AlsoStarted != "~/.config/i3/config:2" || a.Running != 1 {
t.Errorf("picom: %+v", a)
}
if got["user/slack.desktop"].Running != 1 || got["system/polychromatic.desktop"].Running != 1 || got["user/nextcloud.desktop"].Running != 0 {
t.Errorf("running: slack %d, polychromatic %d, nextcloud %d", got["user/slack.desktop"].Running,
got["system/polychromatic.desktop"].Running, got["user/nextcloud.desktop"].Running)
}
}
func TestTheCheckFindsTwoStartersAndAMissingProgram(t *testing.T) {
m := autostartMachine(t)
checks := map[string]Check{}
for _, c := range m.Check("i3")["checks"].([]Check) {
checks[c.Name] = c
}
twice := checks["no program is started twice at login"]
if twice.OK || !strings.Contains(twice.Detail, "picom: by /etc/xdg/autostart/picom.desktop and by ~/.config/i3/config:2") ||
!strings.Contains(twice.Detail, "slack: by ~/.config/autostart/slack.desktop and /etc/xdg/autostart/slack-too.desktop") {
t.Fatalf("%+v", twice)
}
// blueman's system entry is hidden by the user's, so dex starts nothing that is missing.
if c := checks["every program dex starts is installed"]; !c.OK {
t.Fatalf("%+v", c)
}
if c := checks["user-dirs.dirs is the mesh's"]; c.OK {
t.Fatalf("no file, yet %+v", c)
}
}