diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-12 20:14:47 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-12 20:14:47 +0200 |
| commit | 3f8679be9373ee7508d512dfdfc1dda0839c7f90 (patch) | |
| tree | ec02eb075f6c4e90f21baa2fe674e86a2f7f6a62 /internal/engine/roundtrip_test.go | |
| parent | 24a84671ace373ae331fa83a1ff484990f4dff0e (diff) | |
| download | krino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.tar.gz krino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.zip | |
krino: acting — trash, journal, apply, lock, review, undo
Diffstat (limited to 'internal/engine/roundtrip_test.go')
| -rw-r--r-- | internal/engine/roundtrip_test.go | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/internal/engine/roundtrip_test.go b/internal/engine/roundtrip_test.go new file mode 100644 index 0000000..425dc1a --- /dev/null +++ b/internal/engine/roundtrip_test.go @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package engine + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "io/fs" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "krino/internal/journal" + "krino/internal/plan" +) + +// snapshot records every file under root: path, content hash, mode and mtime. +func snapshot(t *testing.T, root string) map[string]string { + t.Helper() + out := map[string]string{} + err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + b, err := os.ReadFile(p) + if err != nil { + return err + } + fi, err := d.Info() + if err != nil { + return err + } + rel, _ := filepath.Rel(root, p) + sum := sha256.Sum256(b) + out[rel] = hex.EncodeToString(sum[:]) + " " + fi.Mode().String() + " " + fi.ModTime().UTC().Format(time.RFC3339Nano) + return nil + }) + if err != nil { + t.Fatal(err) + } + return out +} + +func TestApplyThenUndoRestoresTheTree(t *testing.T) { + h := sandbox(t) + dl := filepath.Join(h, "dl") + if err := os.MkdirAll(filepath.Join(dl, "sub"), 0o755); err != nil { + t.Fatal(err) + } + files := map[string]string{ + "inv1.pdf": "invoice one", + "inv2.pdf": "invoice two", + "notes.txt": "not a pdf", + "sub/deep.pdf": "nested", + } + old := time.Now().Add(-2 * time.Hour) + for rel, body := range files { + p := filepath.Join(dl, rel) + if err := os.WriteFile(p, []byte(body), 0o640); err != nil { + t.Fatal(err) + } + if err := os.Chtimes(p, old, old); err != nil { + t.Fatal(err) + } + } + before := snapshot(t, dl) + + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": ` +(path "~/dl") +(recursive yes) +(rule "pdfs" (when (type pdf)) (copy "~/backup") (move "Work/{mtime:%Y}")) +`}) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + dp, err := e.Plan(context.Background(), e.Dirs[0], plan.NewClaims()) + if err != nil { + t.Fatal(err) + } + approved := map[string]bool{} + for _, c := range dp.Chains { + approved[c.File.Rel] = true + } + logPath := filepath.Join(h, ".local", "state", "krino", "krino.log") + j, err := journal.Open(logPath) + if err != nil { + t.Fatal(err) + } + run := journal.NewRunID(time.Now()) + res, err := e.Apply(context.Background(), dp, approved, j, run) + if err != nil { + t.Fatal(err) + } + j.Close() + if res.Failed != 0 { + t.Fatalf("%d files failed: %+v", res.Failed, res) + } + if reflect.DeepEqual(snapshot(t, dl), before) { + t.Fatal("apply changed nothing") + } + if _, err := os.Stat(filepath.Join(h, "backup", "inv1.pdf")); err != nil { + t.Errorf("the copy did not land: %v", err) + } + + up, err := e.PlanUndo(run) + if err != nil { + t.Fatal(err) + } + for _, f := range up.Files { + if f.Refused != "" { + t.Fatalf("undo refused %s: %s", f.File, f.Refused) + } + } + j2, err := journal.Open(logPath) + if err != nil { + t.Fatal(err) + } + if _, err := e.ApplyUndo(context.Background(), up, j2, journal.NewRunID(time.Now())); err != nil { + t.Fatal(err) + } + j2.Close() + + // Verify the move cycle within dl was undone completely. + got := snapshot(t, dl) + for rel, want := range before { + if got[rel] != want { + t.Errorf("%s after undo:\n got %s\nwant %s", rel, got[rel], want) + } + } + for rel := range got { + if _, ok := before[rel]; !ok { + t.Errorf("%s exists after undo but did not before", rel) + } + } + + // Verify the copy-undo removed all copies from ~/backup. + // undo-copy sends them to trash, so backup should be gone (or exist but + // contain none of inv1.pdf, inv2.pdf, deep.pdf). + backupDir := filepath.Join(h, "backup") + copied := []string{"inv1.pdf", "inv2.pdf", "deep.pdf"} + for _, name := range copied { + p := filepath.Join(backupDir, name) + if _, err := os.Stat(p); err == nil { + t.Errorf("copy %s still exists after undo", name) + } else if !os.IsNotExist(err) { + t.Errorf("checking %s after undo: %v", name, err) + } + } + // Also check that if backupDir exists, it is empty (no copies remain). + if entries, err := os.ReadDir(backupDir); err == nil { + if len(entries) > 0 { + t.Errorf("backup dir not empty after undo: %v", entries) + } + } else if !os.IsNotExist(err) { + t.Errorf("reading backup dir after undo: %v", err) + } +} |
