detached 5d4d89e

test(snapshot): tighten snapshot assertions

commit
5d4d89e8b735536b9d27c709fb45e7afff0a021f
author
brice · · 2026-07-22 04:54 UTC
parent
962bc0c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
6969 repository := &retryablePurgeRepository{incrementalErr: tt.incremental, legacyErr: tt.legacy}
7070 WithSnapshotRepository(repository, repository)(d)
7171 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()
7375 d.sessions = map[domain.SessionID]*session{sess.id: sess}
7476
7577 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
7878 if !ok {
7979 return ports.SnapshotGeneration{}, errors.New("missing generation")
8080 }
81+ generation = cloneAcceptanceGeneration(generation)
8182 if r.loadMutate != nil && len(r.publishes) > 0 {
8283 generation = r.loadMutate(generation, r.publishes[len(r.publishes)-1])
8384 }
@@ -89,6 +90,30 @@ func (*snapshotAcceptanceRepository) Tombstone(context.Context, string) error
8990 func (*snapshotAcceptanceRepository) DeleteTombstone(context.Context, string) error { return nil }
9091 func (*snapshotAcceptanceRepository) Maintain(context.Context) error { return nil }
9192
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+
92117 func cloneAcceptanceGeneration(g ports.SnapshotGeneration) ports.SnapshotGeneration {
93118 out := g
94119 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 := g118 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.
396396 }
397397
398398 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")
400401 pty.EXPECT().Close().Return(nil).Maybe()
401402 d.sessions[sess.id] = sess
402403 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] = sess402 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) {
266266
267267 func TestScreenDropsOversizedHistoryRowsWithoutInterruptingScroll(t *testing.T) {
268268 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)) }
271271 copy(screen.Frame.Row(0), historyRow("AAAA"))
272272 copy(screen.Frame.Row(1), historyRow("BBBB"))
273273 screen.scrollUpRegion(0, 1, 1)
@@ -275,8 +275,8 @@ func TestScreenDropsOversizedHistoryRowsWithoutInterruptingScroll(t *testing.T)
275275 if got := screen.History().Len(); got != 0 {
276276 t.Fatalf("oversized scrollback rows = %d, want 0", got)
277277 }
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)
280280 }
281281 if got := lineText(screen, 0); got != "BBBB" {
282282 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 := 0269+ 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 @@
11 package vt
22
3-import "github.com/bnema/vev/pkg/renderer"
3+import (
4+ "errors"
5+
6+ "github.com/bnema/vev/pkg/renderer"
7+)
48
59 func (s *Screen) index() {
610 if s.Frame.Height == 0 {
@@ -88,9 +92,10 @@ func (s *Screen) emitLineEvicted(top, n int) {
8892
8993 func (s *Screen) recordEvicted(row []renderer.Cell) {
9094 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+ }
9499 }
95100 if s.OnLineEvicted != nil {
96101 s.OnLineEvicted(append([]renderer.Cell(nil), row...))
@@ -1,6 +1,10 @@
1 package vt1 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 must95+ 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...))

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close