detached c002b88

test(daemon): cover the restored generation payload budget

commit
c002b8840d9c27bd8c31c126ef66de30fd86c656
author
brice · · 2026-10-08 08:54 UTC
committer
brice · · 2026-10-08 09:31 UTC
parent
351738d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +44 -3
M internal/usecase/daemon/snapshot_restore_incremental.go +8 -3
@@ -387,6 +387,14 @@ func (d *Daemon) logRejectedRestoreCandidate(record domain.CatalogueRecord, cand
387387 }
388388
389389 func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Session, error) {
390+ // Objects are compressed, so the on-disk read budget no longer bounds the
391+ // inflated generation; budget its payloads as one uncompressed checkpoint.
392+ return sessionFromGenerationBudget(generation, snapcodec.MaxGenerationPayloadBytes)
393+}
394+
395+// sessionFromGenerationBudget decodes a generation whose inflated object
396+// payloads total at most budget bytes.
397+func sessionFromGenerationBudget(generation ports.SnapshotGeneration, budget int) (snapcodec.Session, error) {
390398 manifest, err := snapcodec.UnmarshalManifest(generation.Manifest)
391399 if err != nil {
392400 return snapcodec.Session{}, err
@@ -395,9 +403,6 @@ func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Sessi
395403 return snapcodec.Session{}, fmt.Errorf("snapshot: generation identity mismatch")
396404 }
397405 result := snapcodec.Session{Name: manifest.Name, CreatedAt: manifest.CreatedAt, Active: manifest.Active, Tabs: make([]snapcodec.Tab, 0, len(manifest.Tabs))}
398- // Objects are compressed, so the on-disk read budget no longer bounds the
399- // inflated generation; budget its payloads as one uncompressed checkpoint.
400- budget := snapcodec.MaxGenerationPayloadBytes
401406 object := func(ref snapcodec.ObjectRef, kind snapcodec.ObjectKind) ([]byte, error) {
402407 data, err := generationObject(generation, ref, kind)
403408 if err != nil {
@@ -387,6 +387,14 @@ func (d *Daemon) logRejectedRestoreCandidate(record domain.CatalogueRecord, cand
387 }387 }
388 388
389 func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Session, error) {389 func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Session, error) {
390+ // Objects are compressed, so the on-disk read budget no longer bounds the
391+ // inflated generation; budget its payloads as one uncompressed checkpoint.
392+ return sessionFromGenerationBudget(generation, snapcodec.MaxGenerationPayloadBytes)
393+}
394+
395+// sessionFromGenerationBudget decodes a generation whose inflated object
396+// payloads total at most budget bytes.
397+func sessionFromGenerationBudget(generation ports.SnapshotGeneration, budget int) (snapcodec.Session, error) {
390 manifest, err := snapcodec.UnmarshalManifest(generation.Manifest)398 manifest, err := snapcodec.UnmarshalManifest(generation.Manifest)
391 if err != nil {399 if err != nil {
392 return snapcodec.Session{}, err400 return snapcodec.Session{}, err
@@ -395,9 +403,6 @@ func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Sessi
395 return snapcodec.Session{}, fmt.Errorf("snapshot: generation identity mismatch")403 return snapcodec.Session{}, fmt.Errorf("snapshot: generation identity mismatch")
396 }404 }
397 result := snapcodec.Session{Name: manifest.Name, CreatedAt: manifest.CreatedAt, Active: manifest.Active, Tabs: make([]snapcodec.Tab, 0, len(manifest.Tabs))}405 result := snapcodec.Session{Name: manifest.Name, CreatedAt: manifest.CreatedAt, Active: manifest.Active, Tabs: make([]snapcodec.Tab, 0, len(manifest.Tabs))}
398- // Objects are compressed, so the on-disk read budget no longer bounds the
399- // inflated generation; budget its payloads as one uncompressed checkpoint.
400- budget := snapcodec.MaxGenerationPayloadBytes
401 object := func(ref snapcodec.ObjectRef, kind snapcodec.ObjectKind) ([]byte, error) {406 object := func(ref snapcodec.ObjectRef, kind snapcodec.ObjectKind) ([]byte, error) {
402 data, err := generationObject(generation, ref, kind)407 data, err := generationObject(generation, ref, kind)
403 if err != nil {408 if err != nil {
M internal/usecase/daemon/snapshot_restore_test.go +36 -0
@@ -623,3 +623,39 @@ func TestRestoreIncrementalFallbackAndInvalidObjectMappings(t *testing.T) {
623623 })
624624 }
625625 }
626+
627+// TestRestoreIncrementalPayloadBudget bounds the inflated payloads of one
628+// generation: compressed objects must not restore past the budget.
629+func TestRestoreIncrementalPayloadBudget(t *testing.T) {
630+ generation := acceptanceGeneration(t, restoreAcceptanceSession(t, "budget"), 3)
631+ decoded, err := sessionFromGeneration(generation)
632+ require.NoError(t, err)
633+ total := 0
634+ for _, tab := range decoded.Tabs {
635+ for _, pane := range tab.Panes {
636+ for _, chunk := range pane.SealedChunks {
637+ total += len(chunk)
638+ }
639+ total += len(pane.Tail) + len(pane.Transcript)
640+ }
641+ }
642+ require.Positive(t, total)
643+
644+ for _, tt := range []struct {
645+ name string
646+ budget int
647+ wantErr bool
648+ }{
649+ {name: "exact budget", budget: total},
650+ {name: "one byte short", budget: total - 1, wantErr: true},
651+ } {
652+ t.Run(tt.name, func(t *testing.T) {
653+ _, err := sessionFromGenerationBudget(cloneAcceptanceGeneration(generation), tt.budget)
654+ if tt.wantErr {
655+ require.ErrorContains(t, err, "generation payload too large")
656+ return
657+ }
658+ require.NoError(t, err)
659+ })
660+ }
661+}
@@ -623,3 +623,39 @@ func TestRestoreIncrementalFallbackAndInvalidObjectMappings(t *testing.T) {
623 })623 })
624 }624 }
625 }625 }
626+
627+// TestRestoreIncrementalPayloadBudget bounds the inflated payloads of one
628+// generation: compressed objects must not restore past the budget.
629+func TestRestoreIncrementalPayloadBudget(t *testing.T) {
630+ generation := acceptanceGeneration(t, restoreAcceptanceSession(t, "budget"), 3)
631+ decoded, err := sessionFromGeneration(generation)
632+ require.NoError(t, err)
633+ total := 0
634+ for _, tab := range decoded.Tabs {
635+ for _, pane := range tab.Panes {
636+ for _, chunk := range pane.SealedChunks {
637+ total += len(chunk)
638+ }
639+ total += len(pane.Tail) + len(pane.Transcript)
640+ }
641+ }
642+ require.Positive(t, total)
643+
644+ for _, tt := range []struct {
645+ name string
646+ budget int
647+ wantErr bool
648+ }{
649+ {name: "exact budget", budget: total},
650+ {name: "one byte short", budget: total - 1, wantErr: true},
651+ } {
652+ t.Run(tt.name, func(t *testing.T) {
653+ _, err := sessionFromGenerationBudget(cloneAcceptanceGeneration(generation), tt.budget)
654+ if tt.wantErr {
655+ require.ErrorContains(t, err, "generation payload too large")
656+ return
657+ }
658+ require.NoError(t, err)
659+ })
660+ }
661+}

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 12

Tags 11

↑↓ moveenter openesc close