docs(vt): document snapshot ownership
- commit
- 9db9bf9ba087fc760f90e1bdc2402e6ed9c7c045
- author
- brice · · 2026-07-28 14:11 UTC
- committer
- brice · · 2026-07-29 05:00 UTC
- parent
- ff327ba
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +104 -0
M pkg/vt/doc.go +5 -0
| @@ -5,4 +5,9 @@ | |||
| 5 | 5 | // sequences. Wide runes that fit are stored as a head cell plus a continuation | |
| 6 | 6 | // cell, and resize or edit operations repair row-boundary splits instead of | |
| 7 | 7 | // leaving orphaned halves. | |
| 8 | + | // | |
| 9 | + | // Screen is single-owner. Snapshot returns an owned capture of the active | |
| 10 | + | // visible viewport and renderer-relevant metadata; callers must serialize it | |
| 11 | + | // with Write, Resize, and host mutations. Terminal history remains available | |
| 12 | + | // through HistoryView and HistorySnapshotView rather than ScreenSnapshot. | |
| 8 | 13 | package vt | |
| @@ -5,4 +5,9 @@ | |||||
| 5 | // sequences. Wide runes that fit are stored as a head cell plus a continuation | 5 | // sequences. Wide runes that fit are stored as a head cell plus a continuation | ||
| 6 | // cell, and resize or edit operations repair row-boundary splits instead of | 6 | // cell, and resize or edit operations repair row-boundary splits instead of | ||
| 7 | // leaving orphaned halves. | 7 | // leaving orphaned halves. | ||
| 8 | + | // | |||
| 9 | + | // Screen is single-owner. Snapshot returns an owned capture of the active | |||
| 10 | + | // visible viewport and renderer-relevant metadata; callers must serialize it | |||
| 11 | + | // with Write, Resize, and host mutations. Terminal history remains available | |||
| 12 | + | // through HistoryView and HistorySnapshotView rather than ScreenSnapshot. | |||
| 8 | package vt | 13 | package vt | ||
M pkg/vt/screen_snapshot_test.go +99 -0
| @@ -300,3 +300,102 @@ func TestScreenSnapshotBoundsAreIndependent(t *testing.T) { | |||
| 300 | 300 | }) | |
| 301 | 301 | } | |
| 302 | 302 | } | |
| 303 | + | ||
| 304 | + | func TestScreenSnapshotDoesNotMutateHistoryOrDamage(t *testing.T) { | |
| 305 | + | tests := []struct { | |
| 306 | + | name string | |
| 307 | + | make func() *Screen | |
| 308 | + | }{ | |
| 309 | + | { | |
| 310 | + | name: "screen without history", | |
| 311 | + | make: func() *Screen { | |
| 312 | + | screen := NewScreen(4, 2) | |
| 313 | + | screen.ClearDamage() | |
| 314 | + | screen.Write([]byte("abcdef")) | |
| 315 | + | return screen | |
| 316 | + | }, | |
| 317 | + | }, | |
| 318 | + | { | |
| 319 | + | name: "screen with sealed chunks and mutable tail", | |
| 320 | + | make: func() *Screen { | |
| 321 | + | screen := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 16, MaxCells: 64, ChunkRows: 2}) | |
| 322 | + | screen.Write([]byte("111122223333444455")) | |
| 323 | + | return screen | |
| 324 | + | }, | |
| 325 | + | }, | |
| 326 | + | } | |
| 327 | + | ||
| 328 | + | for _, test := range tests { | |
| 329 | + | t.Run(test.name, func(t *testing.T) { | |
| 330 | + | screen := test.make() | |
| 331 | + | beforeDamage := screen.CaptureDamage() | |
| 332 | + | var beforeHistory HistorySnapshotView | |
| 333 | + | beforeCap, beforeCellCap := 0, 0 | |
| 334 | + | if screen.History() != nil { | |
| 335 | + | beforeHistory = screen.History().SnapshotView() | |
| 336 | + | beforeCap = screen.History().Cap() | |
| 337 | + | beforeCellCap = screen.History().CellCap() | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | _ = screen.Snapshot() | |
| 341 | + | ||
| 342 | + | afterDamage := screen.CaptureDamage() | |
| 343 | + | require.Equal(t, beforeDamage, afterDamage) | |
| 344 | + | if screen.History() != nil { | |
| 345 | + | require.Equal(t, beforeHistory, screen.History().SnapshotView()) | |
| 346 | + | require.Equal(t, beforeCap, screen.History().Cap()) | |
| 347 | + | require.Equal(t, beforeCellCap, screen.History().CellCap()) | |
| 348 | + | } | |
| 349 | + | }) | |
| 350 | + | } | |
| 351 | + | } | |
| 352 | + | ||
| 353 | + | func TestScreenSnapshotPreservesStaleDamageAcknowledge(t *testing.T) { | |
| 354 | + | tests := []struct { | |
| 355 | + | name string | |
| 356 | + | mutate func(*Screen) | |
| 357 | + | }{ | |
| 358 | + | {name: "write after capture", mutate: func(screen *Screen) { screen.Write([]byte("b")) }}, | |
| 359 | + | {name: "resize after capture", mutate: func(screen *Screen) { screen.Resize(6, 2) }}, | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | for _, test := range tests { | |
| 363 | + | t.Run(test.name, func(t *testing.T) { | |
| 364 | + | screen := NewScreen(4, 2) | |
| 365 | + | screen.ClearDamage() | |
| 366 | + | screen.Write([]byte("a")) | |
| 367 | + | captured := screen.CaptureDamage() | |
| 368 | + | ||
| 369 | + | _ = screen.Snapshot() | |
| 370 | + | test.mutate(screen) | |
| 371 | + | ||
| 372 | + | require.False(t, screen.AcknowledgeDamage(captured.Generation)) | |
| 373 | + | require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, screen.Damage()) | |
| 374 | + | ||
| 375 | + | screen.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: 0, Width: 1, Height: 1, Count: 1}) | |
| 376 | + | require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, screen.Damage(), "full redraw stays sticky until exact acknowledgement") | |
| 377 | + | }) | |
| 378 | + | } | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | func TestScreenSnapshotPreservesExactDamageAcknowledge(t *testing.T) { | |
| 382 | + | tests := []struct { | |
| 383 | + | name string | |
| 384 | + | }{ | |
| 385 | + | {name: "snapshot does not stale an exact damage capture"}, | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | for _, test := range tests { | |
| 389 | + | t.Run(test.name, func(t *testing.T) { | |
| 390 | + | screen := NewScreen(4, 2) | |
| 391 | + | screen.ClearDamage() | |
| 392 | + | screen.Write([]byte("a")) | |
| 393 | + | captured := screen.CaptureDamage() | |
| 394 | + | ||
| 395 | + | _ = screen.Snapshot() | |
| 396 | + | ||
| 397 | + | require.True(t, screen.AcknowledgeDamage(captured.Generation)) | |
| 398 | + | require.Empty(t, screen.Damage()) | |
| 399 | + | }) | |
| 400 | + | } | |
| 401 | + | } | |
| @@ -300,3 +300,102 @@ func TestScreenSnapshotBoundsAreIndependent(t *testing.T) { | |||||
| 300 | }) | 300 | }) | ||
| 301 | } | 301 | } | ||
| 302 | } | 302 | } | ||
| 303 | + | ||||
| 304 | + | func TestScreenSnapshotDoesNotMutateHistoryOrDamage(t *testing.T) { | |||
| 305 | + | tests := []struct { | |||
| 306 | + | name string | |||
| 307 | + | make func() *Screen | |||
| 308 | + | }{ | |||
| 309 | + | { | |||
| 310 | + | name: "screen without history", | |||
| 311 | + | make: func() *Screen { | |||
| 312 | + | screen := NewScreen(4, 2) | |||
| 313 | + | screen.ClearDamage() | |||
| 314 | + | screen.Write([]byte("abcdef")) | |||
| 315 | + | return screen | |||
| 316 | + | }, | |||
| 317 | + | }, | |||
| 318 | + | { | |||
| 319 | + | name: "screen with sealed chunks and mutable tail", | |||
| 320 | + | make: func() *Screen { | |||
| 321 | + | screen := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 16, MaxCells: 64, ChunkRows: 2}) | |||
| 322 | + | screen.Write([]byte("111122223333444455")) | |||
| 323 | + | return screen | |||
| 324 | + | }, | |||
| 325 | + | }, | |||
| 326 | + | } | |||
| 327 | + | ||||
| 328 | + | for _, test := range tests { | |||
| 329 | + | t.Run(test.name, func(t *testing.T) { | |||
| 330 | + | screen := test.make() | |||
| 331 | + | beforeDamage := screen.CaptureDamage() | |||
| 332 | + | var beforeHistory HistorySnapshotView | |||
| 333 | + | beforeCap, beforeCellCap := 0, 0 | |||
| 334 | + | if screen.History() != nil { | |||
| 335 | + | beforeHistory = screen.History().SnapshotView() | |||
| 336 | + | beforeCap = screen.History().Cap() | |||
| 337 | + | beforeCellCap = screen.History().CellCap() | |||
| 338 | + | } | |||
| 339 | + | ||||
| 340 | + | _ = screen.Snapshot() | |||
| 341 | + | ||||
| 342 | + | afterDamage := screen.CaptureDamage() | |||
| 343 | + | require.Equal(t, beforeDamage, afterDamage) | |||
| 344 | + | if screen.History() != nil { | |||
| 345 | + | require.Equal(t, beforeHistory, screen.History().SnapshotView()) | |||
| 346 | + | require.Equal(t, beforeCap, screen.History().Cap()) | |||
| 347 | + | require.Equal(t, beforeCellCap, screen.History().CellCap()) | |||
| 348 | + | } | |||
| 349 | + | }) | |||
| 350 | + | } | |||
| 351 | + | } | |||
| 352 | + | ||||
| 353 | + | func TestScreenSnapshotPreservesStaleDamageAcknowledge(t *testing.T) { | |||
| 354 | + | tests := []struct { | |||
| 355 | + | name string | |||
| 356 | + | mutate func(*Screen) | |||
| 357 | + | }{ | |||
| 358 | + | {name: "write after capture", mutate: func(screen *Screen) { screen.Write([]byte("b")) }}, | |||
| 359 | + | {name: "resize after capture", mutate: func(screen *Screen) { screen.Resize(6, 2) }}, | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | for _, test := range tests { | |||
| 363 | + | t.Run(test.name, func(t *testing.T) { | |||
| 364 | + | screen := NewScreen(4, 2) | |||
| 365 | + | screen.ClearDamage() | |||
| 366 | + | screen.Write([]byte("a")) | |||
| 367 | + | captured := screen.CaptureDamage() | |||
| 368 | + | ||||
| 369 | + | _ = screen.Snapshot() | |||
| 370 | + | test.mutate(screen) | |||
| 371 | + | ||||
| 372 | + | require.False(t, screen.AcknowledgeDamage(captured.Generation)) | |||
| 373 | + | require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, screen.Damage()) | |||
| 374 | + | ||||
| 375 | + | screen.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: 0, Width: 1, Height: 1, Count: 1}) | |||
| 376 | + | require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, screen.Damage(), "full redraw stays sticky until exact acknowledgement") | |||
| 377 | + | }) | |||
| 378 | + | } | |||
| 379 | + | } | |||
| 380 | + | ||||
| 381 | + | func TestScreenSnapshotPreservesExactDamageAcknowledge(t *testing.T) { | |||
| 382 | + | tests := []struct { | |||
| 383 | + | name string | |||
| 384 | + | }{ | |||
| 385 | + | {name: "snapshot does not stale an exact damage capture"}, | |||
| 386 | + | } | |||
| 387 | + | ||||
| 388 | + | for _, test := range tests { | |||
| 389 | + | t.Run(test.name, func(t *testing.T) { | |||
| 390 | + | screen := NewScreen(4, 2) | |||
| 391 | + | screen.ClearDamage() | |||
| 392 | + | screen.Write([]byte("a")) | |||
| 393 | + | captured := screen.CaptureDamage() | |||
| 394 | + | ||||
| 395 | + | _ = screen.Snapshot() | |||
| 396 | + | ||||
| 397 | + | require.True(t, screen.AcknowledgeDamage(captured.Generation)) | |||
| 398 | + | require.Empty(t, screen.Damage()) | |||
| 399 | + | }) | |||
| 400 | + | } | |||
| 401 | + | } | |||