first commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// Tests for the file-snapshot rewind journal: per-turn dedup of recorded paths,
|
||||
// commit grouping (an untouched turn produces no restore point), and restore
|
||||
// semantics — reverse replay across turns rolls a file to its oldest baseline,
|
||||
// files that did not exist are deleted, and the journal is truncated to the tip.
|
||||
package agenttool
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeFileT(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func readFileT(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// A turn's first Record for a path is the baseline; later Records that turn are
|
||||
// ignored, and Commit groups the turn's files into one point.
|
||||
func TestRecorderRecordDedupAndCommit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
f := filepath.Join(dir, "a.txt")
|
||||
writeFileT(t, f, "v0")
|
||||
|
||||
r := NewFileSnapshotRecorder()
|
||||
r.Record(f) // baseline "v0"
|
||||
writeFileT(t, f, "v1") // model's first edit
|
||||
r.Record(f) // second edit same turn — must be ignored
|
||||
writeFileT(t, f, "v2")
|
||||
|
||||
if !r.Commit("leaf0", "edit a") {
|
||||
t.Fatal("Commit reported no restore point despite a recorded file")
|
||||
}
|
||||
// An untouched turn creates nothing.
|
||||
if r.Commit("leaf1", "no edits") {
|
||||
t.Fatal("Commit created a restore point for a turn with no records")
|
||||
}
|
||||
points := r.Points()
|
||||
if len(points) != 1 || len(points[0].Snapshots) != 1 {
|
||||
t.Fatalf("want 1 point with 1 snapshot, got %+v", points)
|
||||
}
|
||||
if got := string(points[0].Snapshots[0].Content); got != "v0" {
|
||||
t.Errorf("baseline content = %q, want v0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Restoring rolls files back and deletes ones that did not exist before, and
|
||||
// returns the pre-turn leaf id.
|
||||
func TestRecorderRestore(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
existing := filepath.Join(dir, "keep.txt")
|
||||
created := filepath.Join(dir, "new.txt")
|
||||
writeFileT(t, existing, "orig")
|
||||
|
||||
r := NewFileSnapshotRecorder()
|
||||
|
||||
// Turn 1: edit an existing file.
|
||||
r.Record(existing)
|
||||
writeFileT(t, existing, "edited")
|
||||
r.Commit("leafA", "turn1")
|
||||
|
||||
// Turn 2: create a brand-new file.
|
||||
r.Record(created)
|
||||
writeFileT(t, created, "brand new")
|
||||
r.Commit("leafB", "turn2")
|
||||
|
||||
// Rewind to before turn 1 (index 0): both turns roll back.
|
||||
leaf, restored, warnings, err := r.Restore(0)
|
||||
if err != nil {
|
||||
t.Fatalf("Restore: %v", err)
|
||||
}
|
||||
if leaf != "leafA" {
|
||||
t.Errorf("target leaf = %q, want leafA", leaf)
|
||||
}
|
||||
if len(warnings) != 0 {
|
||||
t.Errorf("unexpected warnings: %v", warnings)
|
||||
}
|
||||
if len(restored) != 2 {
|
||||
t.Errorf("restored %d files, want 2", len(restored))
|
||||
}
|
||||
if got := readFileT(t, existing); got != "orig" {
|
||||
t.Errorf("existing file = %q, want orig", got)
|
||||
}
|
||||
if _, err := os.Stat(created); !os.IsNotExist(err) {
|
||||
t.Errorf("created file should have been deleted, stat err = %v", err)
|
||||
}
|
||||
if len(r.Points()) != 0 {
|
||||
t.Errorf("journal should be empty after restoring from index 0")
|
||||
}
|
||||
}
|
||||
|
||||
// When a file is edited across several turns, restoring to before the earliest
|
||||
// of them lands it at its oldest baseline (not an intermediate version).
|
||||
func TestRecorderRestoreOldestBaselineWins(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
f := filepath.Join(dir, "a.txt")
|
||||
writeFileT(t, f, "v0")
|
||||
|
||||
r := NewFileSnapshotRecorder()
|
||||
r.Record(f) // baseline v0
|
||||
writeFileT(t, f, "v1")
|
||||
r.Commit("leaf0", "t1")
|
||||
|
||||
r.Record(f) // baseline v1
|
||||
writeFileT(t, f, "v2")
|
||||
r.Commit("leaf1", "t2")
|
||||
|
||||
// Rewind to before t2 only (index 1): file returns to v1.
|
||||
if _, _, _, err := r.Restore(1); err != nil {
|
||||
t.Fatalf("Restore(1): %v", err)
|
||||
}
|
||||
if got := readFileT(t, f); got != "v1" {
|
||||
t.Errorf("after rewind to before t2, file = %q, want v1", got)
|
||||
}
|
||||
// One point remains (t1); rewind it too → v0.
|
||||
if _, _, _, err := r.Restore(0); err != nil {
|
||||
t.Fatalf("Restore(0): %v", err)
|
||||
}
|
||||
if got := readFileT(t, f); got != "v0" {
|
||||
t.Errorf("after full rewind, file = %q, want v0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecorderRestoreOutOfRange(t *testing.T) {
|
||||
r := NewFileSnapshotRecorder()
|
||||
if _, _, _, err := r.Restore(0); err == nil {
|
||||
t.Error("Restore on empty journal should error")
|
||||
}
|
||||
}
|
||||
|
||||
// A nil recorder is a safe no-op so tools can hold an optional handle.
|
||||
func TestRecorderNilSafe(t *testing.T) {
|
||||
var r *FileSnapshotRecorder
|
||||
r.Record("/nonexistent")
|
||||
if r.Commit("x", "y") {
|
||||
t.Error("nil Commit should report no point")
|
||||
}
|
||||
if r.Points() != nil {
|
||||
t.Error("nil Points should be nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user