test(snapshot): tighten snapshot assertions
- commit
- 5d4d89e8b735536b9d27c709fb45e7afff0a021f
- author
- brice · · 2026-07-22 04:54 UTC
- parent
- 962bc0c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +43 -10
M internal/usecase/daemon/session_purge_retry_test.go +3 -1
| @@ -69,7 +69,9 @@ func TestLivePurgeRetainsTombstoneAcrossPartialSourceDeletion(t *testing.T) { | |||
| 69 | 69 | repository := &retryablePurgeRepository{incrementalErr: tt.incremental, legacyErr: tt.legacy} | |
| 70 | 70 | WithSnapshotRepository(repository, repository)(d) | |
| 71 | 71 | sess := newSnapshotTestSession(t, "work", false, "/work") | |
| 72 | - | sess.tabs[0].panes["pane-1"].pty.(*portsmocks.MockPTY).EXPECT().Close().Return(nil).Once() | |
| 72 | + | pty, ok := sess.tabs[0].panes["pane-1"].pty.(*portsmocks.MockPTY) | |
| 73 | + | require.True(t, ok, "snapshot test pane must use MockPTY") | |
| 74 | + | pty.EXPECT().Close().Return(nil).Once() | |
| 73 | 75 | d.sessions = map[domain.SessionID]*session{sess.id: sess} | |
| 74 | 76 | ||
| 75 | 77 | require.Error(t, d.killSession(sess, ports.ReasonSessionKilled, true)) | |
| @@ -69,7 +69,9 @@ func TestLivePurgeRetainsTombstoneAcrossPartialSourceDeletion(t *testing.T) { | |||||
| 69 | repository := &retryablePurgeRepository{incrementalErr: tt.incremental, legacyErr: tt.legacy} | 69 | repository := &retryablePurgeRepository{incrementalErr: tt.incremental, legacyErr: tt.legacy} | ||
| 70 | WithSnapshotRepository(repository, repository)(d) | 70 | WithSnapshotRepository(repository, repository)(d) | ||
| 71 | sess := newSnapshotTestSession(t, "work", false, "/work") | 71 | sess := newSnapshotTestSession(t, "work", false, "/work") | ||
| 72 | - | sess.tabs[0].panes["pane-1"].pty.(*portsmocks.MockPTY).EXPECT().Close().Return(nil).Once() | 72 | + | pty, ok := sess.tabs[0].panes["pane-1"].pty.(*portsmocks.MockPTY) |
| 73 | + | require.True(t, ok, "snapshot test pane must use MockPTY") | |||
| 74 | + | pty.EXPECT().Close().Return(nil).Once() | |||
| 73 | d.sessions = map[domain.SessionID]*session{sess.id: sess} | 75 | d.sessions = map[domain.SessionID]*session{sess.id: sess} | ||
| 74 | 76 | ||||
| 75 | require.Error(t, d.killSession(sess, ports.ReasonSessionKilled, true)) | 77 | require.Error(t, d.killSession(sess, ports.ReasonSessionKilled, true)) | ||
M internal/usecase/daemon/snapshot_restore_test.go +25 -0
| @@ -78,6 +78,7 @@ func (r *snapshotAcceptanceRepository) Load(ctx context.Context, name string) (p | |||
| 78 | 78 | if !ok { | |
| 79 | 79 | return ports.SnapshotGeneration{}, errors.New("missing generation") | |
| 80 | 80 | } | |
| 81 | + | generation = cloneAcceptanceGeneration(generation) | |
| 81 | 82 | if r.loadMutate != nil && len(r.publishes) > 0 { | |
| 82 | 83 | generation = r.loadMutate(generation, r.publishes[len(r.publishes)-1]) | |
| 83 | 84 | } | |
| @@ -89,6 +90,30 @@ func (*snapshotAcceptanceRepository) Tombstone(context.Context, string) error | |||
| 89 | 90 | func (*snapshotAcceptanceRepository) DeleteTombstone(context.Context, string) error { return nil } | |
| 90 | 91 | func (*snapshotAcceptanceRepository) Maintain(context.Context) error { return nil } | |
| 91 | 92 | ||
| 93 | + | func TestSnapshotAcceptanceRepositoryLoadMutationDoesNotChangeStoredGeneration(t *testing.T) { | |
| 94 | + | digest := ports.SnapshotDigest{1} | |
| 95 | + | stored := ports.SnapshotGeneration{ | |
| 96 | + | Name: "work", | |
| 97 | + | Manifest: []byte("manifest"), | |
| 98 | + | Objects: map[ports.SnapshotDigest][]byte{digest: []byte("object")}, | |
| 99 | + | } | |
| 100 | + | repository := &snapshotAcceptanceRepository{ | |
| 101 | + | generations: map[string]ports.SnapshotGeneration{"work": stored}, | |
| 102 | + | publishes: []ports.SnapshotPublication{{Name: "work"}}, | |
| 103 | + | loadMutate: func(g ports.SnapshotGeneration, _ ports.SnapshotPublication) ports.SnapshotGeneration { | |
| 104 | + | g.Manifest[0] = 'M' | |
| 105 | + | g.Objects[digest][0] = 'O' | |
| 106 | + | delete(g.Objects, digest) | |
| 107 | + | return g | |
| 108 | + | }, | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | _, err := repository.Load(context.Background(), "work") | |
| 112 | + | require.NoError(t, err) | |
| 113 | + | require.Equal(t, []byte("manifest"), repository.generations["work"].Manifest) | |
| 114 | + | require.Equal(t, []byte("object"), repository.generations["work"].Objects[digest]) | |
| 115 | + | } | |
| 116 | + | ||
| 92 | 117 | func cloneAcceptanceGeneration(g ports.SnapshotGeneration) ports.SnapshotGeneration { | |
| 93 | 118 | out := g | |
| 94 | 119 | out.Manifest = append([]byte(nil), g.Manifest...) | |
| @@ -78,6 +78,7 @@ func (r *snapshotAcceptanceRepository) Load(ctx context.Context, name string) (p | |||||
| 78 | if !ok { | 78 | if !ok { | ||
| 79 | return ports.SnapshotGeneration{}, errors.New("missing generation") | 79 | return ports.SnapshotGeneration{}, errors.New("missing generation") | ||
| 80 | } | 80 | } | ||
| 81 | + | generation = cloneAcceptanceGeneration(generation) | |||
| 81 | if r.loadMutate != nil && len(r.publishes) > 0 { | 82 | if r.loadMutate != nil && len(r.publishes) > 0 { | ||
| 82 | generation = r.loadMutate(generation, r.publishes[len(r.publishes)-1]) | 83 | generation = r.loadMutate(generation, r.publishes[len(r.publishes)-1]) | ||
| 83 | } | 84 | } | ||
| @@ -89,6 +90,30 @@ func (*snapshotAcceptanceRepository) Tombstone(context.Context, string) error | |||||
| 89 | func (*snapshotAcceptanceRepository) DeleteTombstone(context.Context, string) error { return nil } | 90 | func (*snapshotAcceptanceRepository) DeleteTombstone(context.Context, string) error { return nil } | ||
| 90 | func (*snapshotAcceptanceRepository) Maintain(context.Context) error { return nil } | 91 | func (*snapshotAcceptanceRepository) Maintain(context.Context) error { return nil } | ||
| 91 | 92 | ||||
| 93 | + | func TestSnapshotAcceptanceRepositoryLoadMutationDoesNotChangeStoredGeneration(t *testing.T) { | |||
| 94 | + | digest := ports.SnapshotDigest{1} | |||
| 95 | + | stored := ports.SnapshotGeneration{ | |||
| 96 | + | Name: "work", | |||
| 97 | + | Manifest: []byte("manifest"), | |||
| 98 | + | Objects: map[ports.SnapshotDigest][]byte{digest: []byte("object")}, | |||
| 99 | + | } | |||
| 100 | + | repository := &snapshotAcceptanceRepository{ | |||
| 101 | + | generations: map[string]ports.SnapshotGeneration{"work": stored}, | |||
| 102 | + | publishes: []ports.SnapshotPublication{{Name: "work"}}, | |||
| 103 | + | loadMutate: func(g ports.SnapshotGeneration, _ ports.SnapshotPublication) ports.SnapshotGeneration { | |||
| 104 | + | g.Manifest[0] = 'M' | |||
| 105 | + | g.Objects[digest][0] = 'O' | |||
| 106 | + | delete(g.Objects, digest) | |||
| 107 | + | return g | |||
| 108 | + | }, | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | _, err := repository.Load(context.Background(), "work") | |||
| 112 | + | require.NoError(t, err) | |||
| 113 | + | require.Equal(t, []byte("manifest"), repository.generations["work"].Manifest) | |||
| 114 | + | require.Equal(t, []byte("object"), repository.generations["work"].Objects[digest]) | |||
| 115 | + | } | |||
| 116 | + | ||||
| 92 | func cloneAcceptanceGeneration(g ports.SnapshotGeneration) ports.SnapshotGeneration { | 117 | func cloneAcceptanceGeneration(g ports.SnapshotGeneration) ports.SnapshotGeneration { | ||
| 93 | out := g | 118 | out := g | ||
| 94 | out.Manifest = append([]byte(nil), g.Manifest...) | 119 | out.Manifest = append([]byte(nil), g.Manifest...) | ||
M internal/usecase/daemon/snapshot_scheduler_test.go +2 -1
| @@ -396,7 +396,8 @@ func TestForcedSnapshotShutdownTimeoutRetainsRetryableStateAndNotice(t *testing. | |||
| 396 | 396 | } | |
| 397 | 397 | ||
| 398 | 398 | sess := newSnapshotTestSession(t, "work", false, "/work") | |
| 399 | - | pty := sess.tabs[0].panes["pane-1"].pty.(*portsmocks.MockPTY) | |
| 399 | + | pty, ok := sess.tabs[0].panes["pane-1"].pty.(*portsmocks.MockPTY) | |
| 400 | + | require.True(t, ok, "snapshot test pane must use MockPTY") | |
| 400 | 401 | pty.EXPECT().Close().Return(nil).Maybe() | |
| 401 | 402 | d.sessions[sess.id] = sess | |
| 402 | 403 | markSnapshotDirty(sess) | |
| @@ -396,7 +396,8 @@ func TestForcedSnapshotShutdownTimeoutRetainsRetryableStateAndNotice(t *testing. | |||||
| 396 | } | 396 | } | ||
| 397 | 397 | ||||
| 398 | sess := newSnapshotTestSession(t, "work", false, "/work") | 398 | sess := newSnapshotTestSession(t, "work", false, "/work") | ||
| 399 | - | pty := sess.tabs[0].panes["pane-1"].pty.(*portsmocks.MockPTY) | 399 | + | pty, ok := sess.tabs[0].panes["pane-1"].pty.(*portsmocks.MockPTY) |
| 400 | + | require.True(t, ok, "snapshot test pane must use MockPTY") | |||
| 400 | pty.EXPECT().Close().Return(nil).Maybe() | 401 | pty.EXPECT().Close().Return(nil).Maybe() | ||
| 401 | d.sessions[sess.id] = sess | 402 | d.sessions[sess.id] = sess | ||
| 402 | markSnapshotDirty(sess) | 403 | markSnapshotDirty(sess) | ||
M pkg/vt/history_test.go +4 -4
| @@ -266,8 +266,8 @@ func TestHistoryRejectsRowWiderThanCellBudgetWithoutMutation(t *testing.T) { | |||
| 266 | 266 | ||
| 267 | 267 | func TestScreenDropsOversizedHistoryRowsWithoutInterruptingScroll(t *testing.T) { | |
| 268 | 268 | screen := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 2, MaxCells: 3}) | |
| 269 | - | events := 0 | |
| 270 | - | screen.OnLineEvicted = func([]renderer.Cell) { events++ } | |
| 269 | + | var events []string | |
| 270 | + | screen.OnLineEvicted = func(row []renderer.Cell) { events = append(events, rowText(row)) } | |
| 271 | 271 | copy(screen.Frame.Row(0), historyRow("AAAA")) | |
| 272 | 272 | copy(screen.Frame.Row(1), historyRow("BBBB")) | |
| 273 | 273 | screen.scrollUpRegion(0, 1, 1) | |
| @@ -275,8 +275,8 @@ func TestScreenDropsOversizedHistoryRowsWithoutInterruptingScroll(t *testing.T) | |||
| 275 | 275 | if got := screen.History().Len(); got != 0 { | |
| 276 | 276 | t.Fatalf("oversized scrollback rows = %d, want 0", got) | |
| 277 | 277 | } | |
| 278 | - | if got := events; got != 1 { | |
| 279 | - | t.Fatalf("scroll callbacks = %d, want 1", got) | |
| 278 | + | if got, want := events, []string{"AAAA"}; !equalStrings(got, want) { | |
| 279 | + | t.Fatalf("scroll callbacks = %#v, want %#v", got, want) | |
| 280 | 280 | } | |
| 281 | 281 | if got := lineText(screen, 0); got != "BBBB" { | |
| 282 | 282 | t.Fatalf("screen did not scroll after history drop: row 0 = %q", got) | |
| @@ -266,8 +266,8 @@ func TestHistoryRejectsRowWiderThanCellBudgetWithoutMutation(t *testing.T) { | |||||
| 266 | 266 | ||||
| 267 | func TestScreenDropsOversizedHistoryRowsWithoutInterruptingScroll(t *testing.T) { | 267 | func TestScreenDropsOversizedHistoryRowsWithoutInterruptingScroll(t *testing.T) { | ||
| 268 | screen := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 2, MaxCells: 3}) | 268 | screen := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 2, MaxCells: 3}) | ||
| 269 | - | events := 0 | 269 | + | var events []string |
| 270 | - | screen.OnLineEvicted = func([]renderer.Cell) { events++ } | 270 | + | screen.OnLineEvicted = func(row []renderer.Cell) { events = append(events, rowText(row)) } |
| 271 | copy(screen.Frame.Row(0), historyRow("AAAA")) | 271 | copy(screen.Frame.Row(0), historyRow("AAAA")) | ||
| 272 | copy(screen.Frame.Row(1), historyRow("BBBB")) | 272 | copy(screen.Frame.Row(1), historyRow("BBBB")) | ||
| 273 | screen.scrollUpRegion(0, 1, 1) | 273 | screen.scrollUpRegion(0, 1, 1) | ||
| @@ -275,8 +275,8 @@ func TestScreenDropsOversizedHistoryRowsWithoutInterruptingScroll(t *testing.T) | |||||
| 275 | if got := screen.History().Len(); got != 0 { | 275 | if got := screen.History().Len(); got != 0 { | ||
| 276 | t.Fatalf("oversized scrollback rows = %d, want 0", got) | 276 | t.Fatalf("oversized scrollback rows = %d, want 0", got) | ||
| 277 | } | 277 | } | ||
| 278 | - | if got := events; got != 1 { | 278 | + | if got, want := events, []string{"AAAA"}; !equalStrings(got, want) { |
| 279 | - | t.Fatalf("scroll callbacks = %d, want 1", got) | 279 | + | t.Fatalf("scroll callbacks = %#v, want %#v", got, want) |
| 280 | } | 280 | } | ||
| 281 | if got := lineText(screen, 0); got != "BBBB" { | 281 | if got := lineText(screen, 0); got != "BBBB" { | ||
| 282 | t.Fatalf("screen did not scroll after history drop: row 0 = %q", got) | 282 | t.Fatalf("screen did not scroll after history drop: row 0 = %q", got) | ||
M pkg/vt/screen_scroll.go +9 -4
| @@ -1,6 +1,10 @@ | |||
| 1 | 1 | package vt | |
| 2 | 2 | ||
| 3 | - | import "github.com/bnema/vev/pkg/renderer" | |
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/vev/pkg/renderer" | |
| 7 | + | ) | |
| 4 | 8 | ||
| 5 | 9 | func (s *Screen) index() { | |
| 6 | 10 | if s.Frame.Height == 0 { | |
| @@ -88,9 +92,10 @@ func (s *Screen) emitLineEvicted(top, n int) { | |||
| 88 | 92 | ||
| 89 | 93 | func (s *Screen) recordEvicted(row []renderer.Cell) { | |
| 90 | 94 | if s.history != nil { | |
| 91 | - | // Oversized rows cannot fit the configured cell budget. Scrolling must | |
| 92 | - | // still complete (and notify observers), so history drops that row. | |
| 93 | - | _ = s.history.Append(row) | |
| 95 | + | if err := s.history.Append(row); errors.Is(err, ErrHistoryRowTooWide) { | |
| 96 | + | // Oversized rows cannot fit the configured cell budget. History drops | |
| 97 | + | // them, while scrolling and eviction observers proceed below. | |
| 98 | + | } | |
| 94 | 99 | } | |
| 95 | 100 | if s.OnLineEvicted != nil { | |
| 96 | 101 | s.OnLineEvicted(append([]renderer.Cell(nil), row...)) | |
| @@ -1,6 +1,10 @@ | |||||
| 1 | package vt | 1 | package vt | ||
| 2 | 2 | ||||
| 3 | - | import "github.com/bnema/vev/pkg/renderer" | 3 | + | import ( |
| 4 | + | "errors" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/vev/pkg/renderer" | |||
| 7 | + | ) | |||
| 4 | 8 | ||||
| 5 | func (s *Screen) index() { | 9 | func (s *Screen) index() { | ||
| 6 | if s.Frame.Height == 0 { | 10 | if s.Frame.Height == 0 { | ||
| @@ -88,9 +92,10 @@ func (s *Screen) emitLineEvicted(top, n int) { | |||||
| 88 | 92 | ||||
| 89 | func (s *Screen) recordEvicted(row []renderer.Cell) { | 93 | func (s *Screen) recordEvicted(row []renderer.Cell) { | ||
| 90 | if s.history != nil { | 94 | if s.history != nil { | ||
| 91 | - | // Oversized rows cannot fit the configured cell budget. Scrolling must | 95 | + | if err := s.history.Append(row); errors.Is(err, ErrHistoryRowTooWide) { |
| 92 | - | // still complete (and notify observers), so history drops that row. | 96 | + | // Oversized rows cannot fit the configured cell budget. History drops |
| 93 | - | _ = s.history.Append(row) | 97 | + | // them, while scrolling and eviction observers proceed below. |
| 98 | + | } | |||
| 94 | } | 99 | } | ||
| 95 | if s.OnLineEvicted != nil { | 100 | if s.OnLineEvicted != nil { | ||
| 96 | s.OnLineEvicted(append([]renderer.Cell(nil), row...)) | 101 | s.OnLineEvicted(append([]renderer.Cell(nil), row...)) | ||