From 1506c7dcd6032c04c1df6f5785b6dd3dfe4511cc Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Mon, 14 Sep 2026 10:56:21 +0200 Subject: krino: duplicates are found, never deleted A rule combining (duplicate) with a delete action is refused at load, and a file that is a duplicate under any duplicate scope its directory uses gets no delete step from any rule: the plan shows it skipped and the chain continues. A failed duplicate check blocks the delete too. Tests cover (matched), another rule's own condition, a test evaluation skipped, two scopes and a failed check, each checking every copy is still on disk. --- internal/engine/engine_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'internal/engine/engine_test.go') diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index 8dcfcb3..18b35e7 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -213,3 +213,47 @@ func TestContentVariantsComputedAtLoad(t *testing.T) { t.Fatalf("ContentVariants = %+v, want %+v", got, want) } } + +// TestLoadRefusesDuplicateWithDelete is spec §4.5: a (duplicate) test +// anywhere in a rule's condition, and a delete action in the same rule, is +// a load error, however the test is nested. +func TestLoadRefusesDuplicateWithDelete(t *testing.T) { + tests := []struct{ rule, want string }{ + {`(rule "a" (when (duplicate)) (delete))`, + `rule "a": (duplicate) cannot be combined with (delete): duplicates are never deleted, move them aside instead`}, + {`(rule "a" (when (duplicate "Archive")) (delete permanent))`, + `rule "a": (duplicate) cannot be combined with (delete permanent)`}, + {`(rule "a" (when (or (type pdf) (duplicate))) (move "Keep") (delete))`, + `rule "a": (duplicate) cannot be combined with (delete)`}, + {`(rule "a" (when (not (duplicate))) (delete))`, + `rule "a": (duplicate) cannot be combined with (delete)`}, + } + for _, tt := range tests { + h := sandbox(t) + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": `(path "/tmp") +` + tt.rule}) + _, errs := Load(main, "dl") + joined := "" + for _, d := range errs { + joined += d.Error() + "\n" + } + if len(errs) != 1 || !strings.Contains(joined, tt.want) { + t.Errorf("rule %s: errs %v, want %q", tt.rule, errs, tt.want) + } + } +} + +// TestLoadAcceptsDuplicateWithMove: the way §5.5 recommends dealing with +// duplicates loads clean, and so does a delete rule with no duplicate test. +func TestLoadAcceptsDuplicateWithMove(t *testing.T) { + for _, rule := range []string{ + `(rule "dupes" (when (duplicate "Archive")) (move "~/.dupes/") (stop))`, + `(rule "old" (when (type iso) (age > 90d)) (delete))`, + } { + h := sandbox(t) + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": "(path \"/tmp\")\n" + rule}) + if _, errs := Load(main, "dl"); len(errs) != 0 { + t.Errorf("rule %s: errs %v, want none", rule, errs) + } + } +} -- cgit v1.3