// SPDX-License-Identifier: GPL-3.0-or-later package main import ( "os" "path/filepath" "strings" "testing" ) // home gives each test its own HOME with no XDG overrides, and points the // package's stdin seam at something guaranteed non-terminal (fix round // 2026-09-12/item 4): every test that reaches cmdSort's terminal check or // the interactive review must not depend on what the ambient test binary's // stdin happens to be - if that were ever a real terminal, such a test // would silently fall through to the interactive prompt and block on a // keypress instead of failing. func home(t *testing.T) string { t.Helper() h := t.TempDir() t.Setenv("HOME", h) for _, v := range []string{"XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME"} { t.Setenv(v, "") } setNonTerminalStdin(t) return h } // setNonTerminalStdin points the stdin seam (sort.go) at os.DevNull for the // duration of the calling test, restoring it afterward. func setNonTerminalStdin(t *testing.T) { t.Helper() f, err := os.Open(os.DevNull) if err != nil { t.Fatal(err) } t.Cleanup(func() { f.Close() }) old := stdin stdin = f t.Cleanup(func() { stdin = old }) } func TestInitNewCheck(t *testing.T) { h := home(t) if err := os.Mkdir(filepath.Join(h, "dl"), 0o755); err != nil { t.Fatal(err) } code, out, errOut := runCLI(t, "init") if code != 0 || !strings.Contains(out, "created ~/.config/krino/krino.conf") { t.Fatalf("init: %d %q %q", code, out, errOut) } code, out, _ = runCLI(t, "check") if code != 0 || !strings.Contains(out, "no directories included") { t.Fatalf("check, empty: %d %q", code, out) } code, out, errOut = runCLI(t, "new", "dl", "~/dl") if code != 0 || !strings.Contains(out, "created ~/.config/krino/dirs/dl.conf") { t.Fatalf("new: %d %q %q", code, out, errOut) } conf := filepath.Join(h, ".config", "krino", "dirs", "dl.conf") rules := "(path \"~/dl\")\n(rule \"pdfs\" (when (type pdf)) (move \"PDF\") (stop))\n" if err := os.WriteFile(conf, []byte(rules), 0o644); err != nil { t.Fatal(err) } code, out, errOut = runCLI(t, "check") if code != 0 || !strings.Contains(out, "dl ~/dl") || !strings.Contains(out, "pdfs") || !strings.Contains(out, "move PDF, stop") { t.Fatalf("check: %d %q %q", code, out, errOut) } if err := os.WriteFile(conf, []byte(`(path "~/dl") (rule "x" (move PDF))`), 0o644); err != nil { t.Fatal(err) } code, _, errOut = runCLI(t, "check") want := `~/.config/krino/dirs/dl.conf:1:31: rule "x": move takes a string: write (move "PDF")` if code != 2 || !strings.Contains(errOut, want) || !strings.Contains(errOut, "1 problem found") { t.Fatalf("check, broken: %d %q", code, errOut) } } // TestConfigFlagExpandsTilde is item B: -c=~/... is not expanded by the // shell (the ~ comes after =), so krino must expand it itself. func TestConfigFlagExpandsTilde(t *testing.T) { h := home(t) cwd := t.TempDir() t.Chdir(cwd) code, out, errOut := runCLI(t, "-c=~/k/krino.conf", "init") if code != 0 { t.Fatalf("init: %d %q %q", code, out, errOut) } want := filepath.Join(h, "k", "krino.conf") if _, err := os.Stat(want); err != nil { t.Fatalf("%s not created: %v", want, err) } if _, err := os.Lstat(filepath.Join(cwd, "~")); !os.IsNotExist(err) { t.Fatalf(`"~" created in the current directory: %v`, err) } } func TestConfigFlag(t *testing.T) { h := home(t) conf := filepath.Join(h, "elsewhere", "krino.conf") if code, _, errOut := runCLI(t, "init", "-c", conf); code != 0 { t.Fatalf("init -c: %q", errOut) } for _, args := range [][]string{{"-c", conf, "check"}, {"check", "-c", conf}} { if code, out, errOut := runCLI(t, args...); code != 0 || !strings.Contains(out, "config: ~/elsewhere/krino.conf") { t.Errorf("%v: %d %q %q", args, code, out, errOut) } } } func TestCommandErrors(t *testing.T) { home(t) tests := []struct { args []string want string }{ {[]string{"check"}, "not found; create it with: krino init"}, {[]string{"new", "onlyname"}, "usage: krino new NAME PATH"}, {[]string{"init", "extra"}, "init takes no arguments"}, } for _, tt := range tests { if code, _, errOut := runCLI(t, tt.args...); code != 2 || !strings.Contains(errOut, tt.want) { t.Errorf("%v: %d %q, want %q", tt.args, code, errOut, tt.want) } } }