diff options
Diffstat (limited to 'internal/engine/apply_test.go')
| -rw-r--r-- | internal/engine/apply_test.go | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/internal/engine/apply_test.go b/internal/engine/apply_test.go index a6b3185..c832e3a 100644 --- a/internal/engine/apply_test.go +++ b/internal/engine/apply_test.go @@ -1463,3 +1463,82 @@ func TestApplyUndoRetryLogsBothMkdirEntriesAndStillMarksOriginalRunUndone(t *tes t.Errorf("the undo run %q itself must never read as Undone", undoRun) } } + +// TestPermanentDeleteStillRestoresWhatItDisplaced: a chain that overwrites +// and then permanently deletes trashes a file the user owned to make room. +// That file is a different file, and ยง7.4 promises of it: "move the +// existing target to Trash first (logged, so undo restores it)". Walking +// the file's entries used to stop dead at the permanent delete, so the +// displace was never reached and the user's file stayed in the Trash with +// krino reporting the run fully undone. +func TestPermanentDeleteStillRestoresWhatItDisplaced(t *testing.T) { + // A real trash entry, so the reversal is offered rather than refused + // for a reason that has nothing to do with this test. + h := sandbox(t) + entry := filepath.Join(trash.Dir(), "files", "a.pdf") + if err := os.MkdirAll(filepath.Dir(entry), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(entry, []byte("the file that was in the way"), 0o644); err != nil { + t.Fatal(err) + } + fi, err := os.Lstat(entry) + if err != nil { + t.Fatal(err) + } + displaced := filepath.Join(h, "archive", "a.pdf") + info := filepath.Join(trash.Dir(), "info", "a.pdf.trashinfo") + if err := os.MkdirAll(filepath.Dir(info), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(info, []byte("[Trash Info]\nPath="+displaced+"\nDeletionDate=2026-09-17T00:00:00\n"), 0o644); err != nil { + t.Fatal(err) + } + ents := []journal.Entry{ + // Chronological, as the log has them: the displace first, then the + // move that needed the name, then the permanent delete. + {Action: "displace", Status: "ok", File: "a.pdf", Dir: "dl", Step: 1, + Src: displaced, Dst: entry, Detail: "a.pdf", + Size: fi.Size(), ModTime: fi.ModTime()}, + {Action: "move", Status: "ok", File: "a.pdf", Dir: "dl", Step: 1, + Src: filepath.Join(h, "dl", "a.pdf"), Dst: displaced}, + {Action: "delete", Status: "ok", File: "a.pdf", Dir: "dl", Step: 2, + Src: displaced}, + } + uf := planUndoFile("dl", "a.pdf", ents, map[journal.ReversedKey]int{}) + if uf.Refused == "" { + t.Error("the permanently deleted file is no longer refused") + } + + rest, ok := displacedUndoFile("dl", ents, map[journal.ReversedKey]int{}) + if !ok { + t.Fatal("the displaced file was not offered for reversal at all") + } + if rest.Refused != "" { + t.Errorf("the displaced file is refused: %q", rest.Refused) + } + if len(rest.Steps) != 1 || rest.Steps[0].Action != "undo-displace" { + t.Fatalf("steps = %+v; want one undo-displace", rest.Steps) + } + if rest.Steps[0].Dst != displaced { + t.Errorf("the reversal puts the file at %q, want %q", rest.Steps[0].Dst, displaced) + } + if rest.File != displaced { + t.Errorf("the offered file is %q, want the displaced file %q", rest.File, displaced) + } +} + +// TestDisplacedFileIsNotOfferedTwice: when the chain's own file is +// reversible, the displace is reversed as one of its steps, as before - +// the separate offer exists only for the file that cannot be reversed. +func TestDisplacedFileIsNotOfferedTwice(t *testing.T) { + ents := []journal.Entry{ + {Action: "displace", Status: "ok", File: "a.pdf", Dir: "dl", Step: 1, + Src: "/archive/a.pdf", Dst: "/trash/files/a.pdf", Detail: "a.pdf"}, + {Action: "move", Status: "ok", File: "a.pdf", Dir: "dl", Step: 1, + Src: "/dl/a.pdf", Dst: "/archive/a.pdf"}, + } + if _, ok := displacedUndoFile("dl", ents, map[journal.ReversedKey]int{}); ok { + t.Error("a reversible chain's displace was offered a second time on its own") + } +} |
