Skip to content

Commit c2da625

Browse files
author
Yumiue
committed
fix(checkpoint): address rollback undo review
1 parent 53f543c commit c2da625

8 files changed

Lines changed: 447 additions & 83 deletions

File tree

internal/checkpoint/per_edit_snapshot_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,3 +1920,137 @@ func TestRestoreBaseline_ErrorsWhenPathMissingFromBaseline(t *testing.T) {
19201920
t.Fatalf("RestoreBaseline missing path error = %v", err)
19211921
}
19221922
}
1923+
1924+
func TestFinalizeExactForCheckpointPaths_CapturesSelectedCurrentPaths(t *testing.T) {
1925+
store, workdir := newTestStore(t)
1926+
targetA := writeWorkdirFile(t, workdir, "a.txt", "a before\n")
1927+
targetB := writeWorkdirFile(t, workdir, "b.txt", "b before\n")
1928+
if _, err := store.CapturePreWrite(targetA); err != nil {
1929+
t.Fatalf("CapturePreWrite(a): %v", err)
1930+
}
1931+
if _, err := store.CapturePreWrite(targetB); err != nil {
1932+
t.Fatalf("CapturePreWrite(b): %v", err)
1933+
}
1934+
if err := os.WriteFile(targetA, []byte("a source\n"), 0o644); err != nil {
1935+
t.Fatalf("write a source: %v", err)
1936+
}
1937+
if err := os.WriteFile(targetB, []byte("b source\n"), 0o644); err != nil {
1938+
t.Fatalf("write b source: %v", err)
1939+
}
1940+
if _, err := store.FinalizeWithExactState("cp-source"); err != nil {
1941+
t.Fatalf("FinalizeWithExactState: %v", err)
1942+
}
1943+
store.Reset()
1944+
1945+
if err := os.WriteFile(targetA, []byte("a guard\n"), 0o644); err != nil {
1946+
t.Fatalf("write a guard: %v", err)
1947+
}
1948+
if err := os.WriteFile(targetB, []byte("b guard\n"), 0o644); err != nil {
1949+
t.Fatalf("write b guard: %v", err)
1950+
}
1951+
written, err := store.FinalizeExactForCheckpointPaths("cp-guard", "cp-source", []string{"a.txt", "./b.txt", "a.txt"})
1952+
if err != nil {
1953+
t.Fatalf("FinalizeExactForCheckpointPaths: %v", err)
1954+
}
1955+
if !written {
1956+
t.Fatal("FinalizeExactForCheckpointPaths written = false, want true")
1957+
}
1958+
1959+
if err := os.WriteFile(targetA, []byte("a drift\n"), 0o644); err != nil {
1960+
t.Fatalf("write a drift: %v", err)
1961+
}
1962+
if err := os.WriteFile(targetB, []byte("b drift\n"), 0o644); err != nil {
1963+
t.Fatalf("write b drift: %v", err)
1964+
}
1965+
if err := store.RestoreExact(context.Background(), "cp-guard"); err != nil {
1966+
t.Fatalf("RestoreExact(cp-guard): %v", err)
1967+
}
1968+
if got := mustReadFile(t, targetA); got != "a guard\n" {
1969+
t.Fatalf("targetA = %q, want guard", got)
1970+
}
1971+
if got := mustReadFile(t, targetB); got != "b guard\n" {
1972+
t.Fatalf("targetB = %q, want guard", got)
1973+
}
1974+
}
1975+
1976+
func TestFinalizeExactForCheckpointPaths_CapturesDeletedAndCreatedCurrentState(t *testing.T) {
1977+
store, workdir := newTestStore(t)
1978+
deleted := writeWorkdirFile(t, workdir, "deleted.txt", "before delete\n")
1979+
created := filepath.Join(workdir, "created.txt")
1980+
if _, err := store.CapturePreWrite(deleted); err != nil {
1981+
t.Fatalf("CapturePreWrite(deleted): %v", err)
1982+
}
1983+
if _, err := store.CapturePreWrite(created); err != nil {
1984+
t.Fatalf("CapturePreWrite(created): %v", err)
1985+
}
1986+
if err := os.Remove(deleted); err != nil {
1987+
t.Fatalf("remove deleted current: %v", err)
1988+
}
1989+
if err := os.WriteFile(created, []byte("created current\n"), 0o644); err != nil {
1990+
t.Fatalf("write created current: %v", err)
1991+
}
1992+
if _, err := store.FinalizeWithExactState("cp-source"); err != nil {
1993+
t.Fatalf("FinalizeWithExactState: %v", err)
1994+
}
1995+
store.Reset()
1996+
1997+
written, err := store.FinalizeExactForCheckpointPaths("cp-guard", "cp-source", []string{"deleted.txt", "created.txt"})
1998+
if err != nil {
1999+
t.Fatalf("FinalizeExactForCheckpointPaths: %v", err)
2000+
}
2001+
if !written {
2002+
t.Fatal("FinalizeExactForCheckpointPaths written = false, want true")
2003+
}
2004+
2005+
if err := os.WriteFile(deleted, []byte("deleted drift\n"), 0o644); err != nil {
2006+
t.Fatalf("write deleted drift: %v", err)
2007+
}
2008+
if err := os.Remove(created); err != nil {
2009+
t.Fatalf("remove created drift: %v", err)
2010+
}
2011+
if err := store.RestoreExact(context.Background(), "cp-guard"); err != nil {
2012+
t.Fatalf("RestoreExact(cp-guard): %v", err)
2013+
}
2014+
if _, err := os.Stat(deleted); !os.IsNotExist(err) {
2015+
t.Fatalf("deleted should be absent after exact restore, stat err = %v", err)
2016+
}
2017+
if got := mustReadFile(t, created); got != "created current\n" {
2018+
t.Fatalf("created = %q, want current", got)
2019+
}
2020+
}
2021+
2022+
func TestFinalizeExactForCheckpointPaths_ValidatesInputsAndPaths(t *testing.T) {
2023+
store, workdir := newTestStore(t)
2024+
target := writeWorkdirFile(t, workdir, "tracked.txt", "before\n")
2025+
if _, err := store.CapturePreWrite(target); err != nil {
2026+
t.Fatalf("CapturePreWrite: %v", err)
2027+
}
2028+
if err := os.WriteFile(target, []byte("after\n"), 0o644); err != nil {
2029+
t.Fatalf("write after: %v", err)
2030+
}
2031+
if _, err := store.FinalizeWithExactState("cp-source"); err != nil {
2032+
t.Fatalf("FinalizeWithExactState: %v", err)
2033+
}
2034+
store.Reset()
2035+
2036+
tests := []struct {
2037+
name string
2038+
checkpoint string
2039+
source string
2040+
paths []string
2041+
want string
2042+
}{
2043+
{name: "empty checkpoint", checkpoint: "", source: "cp-source", paths: []string{"tracked.txt"}, want: "empty checkpointID"},
2044+
{name: "empty source", checkpoint: "cp-guard", source: "", paths: []string{"tracked.txt"}, want: "source checkpoint id required"},
2045+
{name: "empty paths", checkpoint: "cp-guard", source: "cp-source", paths: nil, want: "exact snapshot paths required"},
2046+
{name: "missing path", checkpoint: "cp-guard", source: "cp-source", paths: []string{"missing.txt"}, want: "baseline version for path missing.txt not found"},
2047+
}
2048+
for _, tt := range tests {
2049+
t.Run(tt.name, func(t *testing.T) {
2050+
_, err := store.FinalizeExactForCheckpointPaths(tt.checkpoint, tt.source, tt.paths)
2051+
if err == nil || !strings.Contains(err.Error(), tt.want) {
2052+
t.Fatalf("FinalizeExactForCheckpointPaths error = %v, want %q", err, tt.want)
2053+
}
2054+
})
2055+
}
2056+
}

internal/runtime/checkpoint_flow_test.go

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -481,18 +481,25 @@ func TestUndoRestoreCheckpoint_RestoresBaselineRollbackGuardPaths(t *testing.T)
481481
fixture := newRuntimeCheckpointFixture(t)
482482
targetA := filepath.Join(fixture.workdir, "baseline-a.txt")
483483
targetB := filepath.Join(fixture.workdir, "baseline-b.txt")
484+
targetC := filepath.Join(fixture.workdir, "baseline-c.txt")
484485
if err := os.WriteFile(targetA, []byte("a before"), 0o644); err != nil {
485486
t.Fatalf("WriteFile(targetA before) error = %v", err)
486487
}
487488
if err := os.WriteFile(targetB, []byte("b before"), 0o644); err != nil {
488489
t.Fatalf("WriteFile(targetB before) error = %v", err)
489490
}
491+
if err := os.WriteFile(targetC, []byte("c before"), 0o644); err != nil {
492+
t.Fatalf("WriteFile(targetC before) error = %v", err)
493+
}
490494
if _, err := fixture.perEditStore.CapturePreWrite(targetA); err != nil {
491495
t.Fatalf("CapturePreWrite(targetA) error = %v", err)
492496
}
493497
if _, err := fixture.perEditStore.CapturePreWrite(targetB); err != nil {
494498
t.Fatalf("CapturePreWrite(targetB) error = %v", err)
495499
}
500+
if _, err := fixture.perEditStore.CapturePreWrite(targetC); err != nil {
501+
t.Fatalf("CapturePreWrite(targetC) error = %v", err)
502+
}
496503

497504
state := newRunState("run-baseline-undo", fixture.session)
498505
if err := fixture.service.createStartOfTurnCheckpoint(context.Background(), &state); err != nil {
@@ -515,20 +522,26 @@ func TestUndoRestoreCheckpoint_RestoresBaselineRollbackGuardPaths(t *testing.T)
515522
if err := os.WriteFile(targetB, []byte("b after"), 0o644); err != nil {
516523
t.Fatalf("WriteFile(targetB after) error = %v", err)
517524
}
525+
if err := os.WriteFile(targetC, []byte("c after"), 0o644); err != nil {
526+
t.Fatalf("WriteFile(targetC after) error = %v", err)
527+
}
518528

519529
if _, err := fixture.service.RestoreCheckpoint(context.Background(), GatewayRestoreInput{
520530
SessionID: fixture.session.ID,
521531
CheckpointID: cpRecord.CheckpointID,
522532
Mode: "baseline",
523-
Paths: []string{"baseline-a.txt"},
533+
Paths: []string{"baseline-a.txt", "baseline-b.txt"},
524534
}); err != nil {
525535
t.Fatalf("RestoreCheckpoint(baseline) error = %v", err)
526536
}
527537
if got := string(mustReadRuntimeFile(t, targetA)); got != "a before" {
528538
t.Fatalf("baseline restored targetA = %q, want a before", got)
529539
}
530-
if got := string(mustReadRuntimeFile(t, targetB)); got != "b after" {
531-
t.Fatalf("baseline restored targetB = %q, want b after", got)
540+
if got := string(mustReadRuntimeFile(t, targetB)); got != "b before" {
541+
t.Fatalf("baseline restored targetB = %q, want b before", got)
542+
}
543+
if got := string(mustReadRuntimeFile(t, targetC)); got != "c after" {
544+
t.Fatalf("baseline restored targetC = %q, want c after", got)
532545
}
533546

534547
if _, err := fixture.service.UndoRestoreCheckpoint(context.Background(), fixture.session.ID); err != nil {
@@ -540,6 +553,9 @@ func TestUndoRestoreCheckpoint_RestoresBaselineRollbackGuardPaths(t *testing.T)
540553
if got := string(mustReadRuntimeFile(t, targetB)); got != "b after" {
541554
t.Fatalf("undo targetB = %q, want b after", got)
542555
}
556+
if got := string(mustReadRuntimeFile(t, targetC)); got != "c after" {
557+
t.Fatalf("undo targetC = %q, want c after", got)
558+
}
543559
}
544560

545561
func TestRestoreCheckpointBaselineRejectsPathsThatNormalizeEmpty(t *testing.T) {
@@ -606,6 +622,81 @@ func TestRestoreCheckpointBaselineWrapsRestoreBaselineError(t *testing.T) {
606622
}
607623
}
608624

625+
func TestRestoreCheckpointBaselineMarksGuardBrokenWhenRestoreFails(t *testing.T) {
626+
fixture := newRuntimeCheckpointFixture(t)
627+
target := filepath.Join(fixture.workdir, "baseline.txt")
628+
if err := os.WriteFile(target, []byte("before baseline"), 0o644); err != nil {
629+
t.Fatalf("WriteFile(before baseline) error = %v", err)
630+
}
631+
if _, err := fixture.perEditStore.CapturePreWrite(target); err != nil {
632+
t.Fatalf("CapturePreWrite() error = %v", err)
633+
}
634+
635+
state := newRunState("run-baseline-failed-restore", fixture.session)
636+
if err := fixture.service.createStartOfTurnCheckpoint(context.Background(), &state); err != nil {
637+
t.Fatalf("createStartOfTurnCheckpoint() error = %v", err)
638+
}
639+
records, err := fixture.checkpointStore.ListCheckpoints(context.Background(), fixture.session.ID, checkpoint.ListCheckpointOpts{})
640+
if err != nil {
641+
t.Fatalf("ListCheckpoints() error = %v", err)
642+
}
643+
if len(records) != 1 {
644+
t.Fatalf("records = %#v, want 1", records)
645+
}
646+
cpRecord := records[0]
647+
if err := fixture.checkpointStore.UpdateCheckpointStatus(context.Background(), cpRecord.CheckpointID, agentsession.CheckpointStatusAvailable); err != nil {
648+
t.Fatalf("UpdateCheckpointStatus() error = %v", err)
649+
}
650+
if err := os.Remove(target); err != nil {
651+
t.Fatalf("Remove(target) error = %v", err)
652+
}
653+
if err := os.Mkdir(target, 0o755); err != nil {
654+
t.Fatalf("Mkdir(target) error = %v", err)
655+
}
656+
657+
_, _, err = fixture.service.restoreCheckpointBaseline(context.Background(), fixture.session.ID, cpRecord.CheckpointID, []string{"baseline.txt"})
658+
if err == nil || !strings.Contains(err.Error(), "baseline restore code") {
659+
t.Fatalf("restoreCheckpointBaseline() error = %v, want baseline restore code", err)
660+
}
661+
662+
records, err = fixture.checkpointStore.ListCheckpoints(context.Background(), fixture.session.ID, checkpoint.ListCheckpointOpts{})
663+
if err != nil {
664+
t.Fatalf("ListCheckpoints(all) error = %v", err)
665+
}
666+
seenBrokenGuard := false
667+
for _, record := range records {
668+
if record.Reason != agentsession.CheckpointReasonGuard {
669+
continue
670+
}
671+
if record.Status != agentsession.CheckpointStatusBroken {
672+
t.Fatalf("guard status = %q, want broken", record.Status)
673+
}
674+
if record.CodeCheckpointRef != "" {
675+
if perEditID := checkpoint.PerEditCheckpointIDFromRef(record.CodeCheckpointRef); perEditID != "" {
676+
if err := fixture.perEditStore.RestoreExact(context.Background(), perEditID); err == nil {
677+
t.Fatalf("failed restore guard %q still has restorable per-edit metadata", perEditID)
678+
}
679+
}
680+
}
681+
seenBrokenGuard = true
682+
}
683+
if !seenBrokenGuard {
684+
t.Fatal("expected failed baseline restore to leave a broken guard record")
685+
}
686+
687+
available, err := fixture.checkpointStore.ListCheckpoints(context.Background(), fixture.session.ID, checkpoint.ListCheckpointOpts{
688+
RestorableOnly: true,
689+
})
690+
if err != nil {
691+
t.Fatalf("ListCheckpoints(restorable) error = %v", err)
692+
}
693+
for _, record := range available {
694+
if record.Reason == agentsession.CheckpointReasonGuard {
695+
t.Fatalf("broken guard %q should not be returned as restorable", record.CheckpointID)
696+
}
697+
}
698+
}
699+
609700
func TestUndoRestoreCheckpoint_RestoresGuardState(t *testing.T) {
610701
fixture := newRuntimeCheckpointFixture(t)
611702
target := filepath.Join(fixture.workdir, "undo.txt")

internal/runtime/checkpoint_restore.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ func (s *Service) restoreCheckpointBaseline(
372372
return RestoreResult{}, agentsession.CheckpointRecord{}, fmt.Errorf("checkpoint: create baseline guard: %w", guardErr)
373373
}
374374
if err := s.perEditStore.RestoreBaseline(ctx, perEditID, relPaths); err != nil {
375+
if guardWritten {
376+
_ = s.perEditStore.DeleteCheckpoint(guardID)
377+
}
378+
_ = s.checkpointStore.UpdateCheckpointStatus(ctx, guardRecord.CheckpointID, agentsession.CheckpointStatusBroken)
375379
return RestoreResult{}, agentsession.CheckpointRecord{}, fmt.Errorf("checkpoint: baseline restore code: %w", err)
376380
}
377381
return RestoreResult{CheckpointID: checkpointID, SessionID: sessionID}, guardRecord, nil

0 commit comments

Comments
 (0)