1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"krino/internal/plan"
"krino/internal/scan"
)
// TestRelWidthAndPadCellCountRunes: C4. relWidth and padCell must measure
// column width in runes, not bytes, or a name carrying diacritics
// misaligns its column - "próba.txt" is 9 runes but 10 bytes (ó is a
// two-byte UTF-8 sequence), one column narrower than its byte length
// would suggest.
func TestRelWidthAndPadCellCountRunes(t *testing.T) {
rels := []string{"a.txt", "próba.txt"}
if w := relWidth(rels); w != 9 {
t.Fatalf("relWidth(%q) = %d, want 9 (rune count of próba.txt, not its %d bytes)", rels, w, len("próba.txt"))
}
if got, want := padCell("a.txt", 9), "a.txt "; got != want {
t.Fatalf("padCell(%q, 9) = %q, want %q", "a.txt", got, want)
}
if got, want := padCell("próba.txt", 9), "próba.txt"; got != want {
t.Fatalf("padCell(%q, 9) = %q, want %q (already at width: no padding)", "próba.txt", got, want)
}
}
// TestActionableChainsAgreesWithCountActing is fix wave item 4 / Minor 5:
// countActing (render.go) and actionableChains used to disagree over a
// chain every one of whose steps is skipped (len(Steps) > 0, but every
// step's own Skip is set) - countActing already excluded it from "to act
// on", while actionableChains's own len(Steps) > 0 check still offered it
// for approval, so a directory could print "N scanned · 0 to act on" and
// then still ask the user to approve a file it had just said there were
// none of. Converged on chainActing (render.go), both must now agree.
func TestActionableChainsAgreesWithCountActing(t *testing.T) {
chains := []plan.Chain{
{File: scan.File{Rel: "a.txt"}, Steps: []plan.Step{{Kind: plan.Move, Skip: "target exists"}}},
{File: scan.File{Rel: "b.txt"}, Steps: []plan.Step{{Kind: plan.Move, Dst: "/r/W/b.txt"}}},
}
if got := countActing(chains); got != 1 {
t.Errorf("countActing = %d, want 1 (a.txt is all-skipped)", got)
}
actionable := actionableChains(chains)
if len(actionable) != 1 || actionable[0].File.Rel != "b.txt" {
t.Errorf("actionableChains = %+v, want only b.txt - an all-skipped chain must never be offered for approval", actionable)
}
}
// TestAllSkippedDirectoryReportsZeroAndLogsNothing is fix wave item 4 /
// Minor 5 and 6, end to end. Before the fix: a directory whose one file
// matches a rule under (on-conflict skip) - so its single step's own Skip
// is set ("target exists") - printed "0 to act on" (countActing) and then,
// with -y, still ran that chain through Apply anyway (actionableChains'
// own len(Steps) > 0 check approved it regardless), logging a
// run-start/run-end pair holding only a "skipped" entry while the outcome
// line read "0 applied · 0 failed · 0 declined" for a file that had just
// been silently processed. After the fix, the chain is never offered for
// approval, Apply is never even called for this directory, and the journal
// gains nothing at all.
func TestAllSkippedDirectoryReportsZeroAndLogsNothing(t *testing.T) {
h := home(t)
dl := filepath.Join(h, "dl")
if err := os.MkdirAll(filepath.Join(dl, "Out"), 0o755); err != nil {
t.Fatal(err)
}
old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
for _, p := range []string{filepath.Join(dl, "a.txt"), filepath.Join(dl, "Out", "a.txt")} {
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Chtimes(p, old, old); err != nil {
t.Fatal(err)
}
}
if code, _, errOut := runCLI(t, "init"); code != 0 {
t.Fatal(errOut)
}
if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 {
t.Fatal(errOut)
}
rules := "(path \"~/dl\")\n(min-age 0s)\n(on-conflict skip)\n(rule \"r\" (when (type text)) (move \"Out\"))\n"
if err := os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(rules), 0o644); err != nil {
t.Fatal(err)
}
code, out, errOut := runCLI(t, "-y")
if code != 0 {
t.Fatalf("run: %d %s", code, errOut)
}
if !strings.Contains(out, "1 scanned · 0 to act on") {
t.Errorf("output = %q, want \"0 to act on\"", out)
}
if !strings.Contains(out, zeroOutcome) {
t.Errorf("output = %q, want the honest zero outcome %q", out, zeroOutcome)
}
logPath := filepath.Join(h, ".local", "state", "krino", "krino.log")
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("reading the journal: %v", err)
}
if len(data) != 0 {
t.Errorf("journal gained entries for a directory with nothing to act on:\n%s", data)
}
}
|