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/plan/chain.go | 8 ++++++++ internal/plan/chain_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) (limited to 'internal/plan') diff --git a/internal/plan/chain.go b/internal/plan/chain.go index bfa2484..e504504 100644 --- a/internal/plan/chain.go +++ b/internal/plan/chain.go @@ -32,6 +32,10 @@ func NewClaims() *Claims { type Input struct { File scan.File Rules []RuleMatch + // NoDelete, when non-empty, skips every delete step of this file with + // this text as the step's Skip, without ending the chain (spec §5.5 + // rule 2, §7.1). + NoDelete string } // Build turns each file's matching rules into a chain. root is the @@ -166,6 +170,10 @@ func buildOne(root string, in Input, now time.Time, d Disk, claim claimed) Chain } case config.Delete, config.DeletePermanent: + if in.NoDelete != "" { + step.Skip = in.NoDelete + break + } deletedBy = rule.Name } diff --git a/internal/plan/chain_test.go b/internal/plan/chain_test.go index b6e3dfe..28c208f 100644 --- a/internal/plan/chain_test.go +++ b/internal/plan/chain_test.go @@ -141,3 +141,31 @@ func TestBuildKeepsSteplessChains(t *testing.T) { } } } + +// TestBuildNoDeleteSkipsDeletesAndContinues is spec §5.5 rule 2 and §7.1: +// with Input.NoDelete set, every delete step is skipped with that reason, +// and the chain carries on from the file's current path instead of ending. +func TestBuildNoDeleteSkipsDeletesAndContinues(t *testing.T) { + in := []Input{{ + File: file("/r", "b.pdf"), + NoDelete: "a duplicate is never deleted", + Rules: []RuleMatch{ + {Name: "old", Actions: []config.Action{act(config.DeletePermanent, "")}}, + {Name: "dupes", Actions: []config.Action{act(config.Move, "Dupes")}}, + {Name: "cleanup", Actions: []config.Action{act(config.Delete, "")}}, + }, + }} + steps := Build("/r", in, time.Now(), NoDisk{}, NewClaims())[0].Steps + if len(steps) != 3 { + t.Fatalf("steps = %+v", steps) + } + if steps[0].Kind != DeletePermanent || steps[0].Skip != "a duplicate is never deleted" { + t.Errorf("step 0 = %+v, want a skipped permanent delete", steps[0]) + } + if steps[1].Kind != Move || steps[1].Skip != "" || steps[1].Dst != "/r/Dupes/b.pdf" { + t.Errorf("step 1 = %+v, want the move to run", steps[1]) + } + if steps[2].Kind != Trash || steps[2].Skip != "a duplicate is never deleted" || steps[2].Src != "/r/Dupes/b.pdf" { + t.Errorf("step 2 = %+v, want a skipped trash from the moved path", steps[2]) + } +} -- cgit v1.3