detached da9eec1

test(snapshot): simplify pane rearrangement restore coverage

commit
da9eec17f5b9f0890cd2e65b81abe6678a5399e8
author
brice · · 2026-07-27 14:30 UTC
parent
db8c344
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +125 -99
M internal/usecase/daemon/snapshot_restore_test.go +125 -99
@@ -40,11 +40,7 @@ func (r *snapshotAcceptanceRepository) Publish(ctx context.Context, p ports.Snap
4040 if r.publishErr != nil {
4141 return r.publishErr
4242 }
43- objects := make(map[ports.SnapshotDigest][]byte, len(p.Objects))
44- for _, object := range p.Objects {
45- objects[object.Digest] = append([]byte(nil), object.Data...)
46- }
47- r.generations[p.Name] = ports.SnapshotGeneration{IncarnationID: p.IncarnationID, Name: p.Name, Generation: p.Generation, ParentCheckpoint: p.ParentCheckpoint, Manifest: append([]byte(nil), p.Manifest...), Objects: objects}
43+ r.generations[p.Name] = cloneAcceptanceGeneration(snapshotGeneration(p))
4844 found := false
4945 for _, name := range r.names {
5046 found = found || name == p.Name
@@ -182,11 +178,9 @@ func acceptanceGeneration(t *testing.T, snapshot snapcodec.Session, generation u
182178 manifest.Generation = generation
183179 encoded, err := snapcodec.MarshalManifest(manifest)
184180 require.NoError(t, err)
185- objects := make(map[ports.SnapshotDigest][]byte, len(publication.Objects))
186- for _, object := range publication.Objects {
187- objects[object.Digest] = append([]byte(nil), object.Data...)
188- }
189- return ports.SnapshotGeneration{IncarnationID: publication.IncarnationID, Name: snapshot.Name, Generation: generation, ParentCheckpoint: publication.ParentCheckpoint, Manifest: encoded, Objects: objects}
181+ publication.Generation = generation
182+ publication.Manifest = encoded
183+ return cloneAcceptanceGeneration(snapshotGeneration(publication))
190184 }
191185
192186 func TestValidateRestoreSessionSnapshot(t *testing.T) {
@@ -257,8 +251,82 @@ func TestRestoreIncrementalGenerationAcceptance(t *testing.T) {
257251 require.Equal(t, uint64(10), repository.publishes[0].Generation, "a restored session must continue the concrete repository generation stream")
258252 }
259253
260-func TestSnapshotCaptureRestorePreservesConsumedAndExpelledPaneLayout(t *testing.T) {
261- initial := &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Weight: 9, Children: []*layout.Node{
254+type paneLayoutSnapshotFixture struct {
255+ snapshot snapcodec.Session
256+ tree *layout.Tree
257+ tabStableID string
258+ paneStableIDs map[layout.PaneID]string
259+}
260+
261+type restoredPaneLayout struct {
262+ tree *layout.Tree
263+ tabStableID string
264+ paneStableIDs map[layout.PaneID]string
265+}
266+
267+func TestSnapshotCodecPreservesConsumedAndExpelledPaneLayout(t *testing.T) {
268+ fixture := captureConsumedAndExpelledPaneLayout(t)
269+ require.Len(t, fixture.snapshot.Tabs, 1)
270+ decodedTab := fixture.snapshot.Tabs[0]
271+
272+ require.Equal(t, fixture.tabStableID, decodedTab.StableID)
273+ require.Equal(t, layout.PaneID("pane-3"), decodedTab.Focus, "snapshot tab focus must be preserved")
274+ assertConsumedAndExpelledColumns(t, decodedTab.Tree)
275+ require.Equal(t, fixture.tree, decodedTab.Tree, "manifest codec must preserve the complete rearranged tree")
276+ require.Equal(t, fixture.paneStableIDs, snapshotPaneStableIDs(decodedTab.Panes), "manifest codec must preserve every pane stable ID")
277+}
278+
279+func TestSnapshotRuntimeRestorePreservesConsumedAndExpelledPaneLayout(t *testing.T) {
280+ fixture := captureConsumedAndExpelledPaneLayout(t)
281+ restoreDaemon := newTestDaemon(t, newFactory(t, &paneRearrangePTY{}), stubClock{})
282+ restoredTabs, err := restoreDaemon.restoreSnapshotTabs(context.Background(), restoreDaemon.serveCtx, fixture.snapshot)
283+ require.NoError(t, err)
284+ require.Len(t, restoredTabs, 1)
285+ defer closeRestoredTabs(restoredTabs)
286+
287+ restored := captureRestoredPaneLayout(restoredTabs[0])
288+ require.Equal(t, fixture.tabStableID, restored.tabStableID)
289+ assertConsumedAndExpelledColumns(t, restored.tree)
290+ require.Equal(t, fixture.tree, restored.tree, "runtime restore must preserve the complete rearranged tree")
291+ require.Equal(t, fixture.paneStableIDs, restored.paneStableIDs, "runtime restore must preserve every pane stable ID")
292+}
293+
294+func captureConsumedAndExpelledPaneLayout(t *testing.T) paneLayoutSnapshotFixture {
295+ t.Helper()
296+ h := newPaneRearrangeHarness(t, domain.Size{Cols: 200, Rows: 30}, paneLayoutSnapshotTree())
297+ h.session.createdAt = 77
298+ h.session.incarnation = domain.IncarnationID{7}
299+ tabStableID := "tab-stable-rearranged"
300+ paneStableIDs := map[layout.PaneID]string{
301+ "pane-1": "pane-stable-1",
302+ "pane-2": "pane-stable-2",
303+ "pane-3": "pane-stable-3",
304+ "pane-4": "pane-stable-4",
305+ "pane-5": "pane-stable-5",
306+ }
307+ setPaneLayoutSnapshotIDs(h.tab, tabStableID, paneStableIDs)
308+ applyPaneLayoutSnapshotOperations(t, h)
309+
310+ h.tab.mu.Lock()
311+ wantTree := h.tab.tree.Clone()
312+ h.tab.mu.Unlock()
313+ capture, ok := h.daemon.captureSnapshotState(h.session, 7)
314+ require.True(t, ok)
315+ publication, err := h.daemon.incrementalPublication(capture)
316+ require.NoError(t, err)
317+ decoded, err := sessionFromGeneration(snapshotGeneration(publication))
318+ require.NoError(t, err)
319+
320+ return paneLayoutSnapshotFixture{
321+ snapshot: decoded,
322+ tree: wantTree,
323+ tabStableID: tabStableID,
324+ paneStableIDs: paneStableIDs,
325+ }
326+}
327+
328+func paneLayoutSnapshotTree() *layout.Tree {
329+ return &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Weight: 9, Children: []*layout.Node{
262330 {Kind: layout.Leaf, Leaf: "pane-1", Weight: 40},
263331 {Kind: layout.Split, Dir: layout.Vertical, Weight: 80, Children: []*layout.Node{
264332 {Kind: layout.Leaf, Leaf: "pane-2", Weight: 10},
@@ -269,110 +337,68 @@ func TestSnapshotCaptureRestorePreservesConsumedAndExpelledPaneLayout(t *testing
269337 {Kind: layout.Leaf, Leaf: "pane-5", Weight: 3},
270338 }},
271339 }}, Focus: "pane-4"}
272- h := newPaneRearrangeHarness(t, domain.Size{Cols: 200, Rows: 30}, initial)
273- h.session.createdAt = 77
274- h.session.incarnation = domain.IncarnationID{7}
275- h.tab.mu.Lock()
276- h.tab.stableID = "tab-stable-rearranged"
277- h.tab.nextPaneID = 6
278- paneStableIDs := map[layout.PaneID]string{
279- "pane-1": "pane-stable-1",
280- "pane-2": "pane-stable-2",
281- "pane-3": "pane-stable-3",
282- "pane-4": "pane-stable-4",
283- "pane-5": "pane-stable-5",
284- }
340+}
341+
342+func setPaneLayoutSnapshotIDs(tb *tab, tabStableID string, paneStableIDs map[layout.PaneID]string) {
343+ tb.mu.Lock()
344+ defer tb.mu.Unlock()
345+ tb.stableID = tabStableID
346+ tb.nextPaneID = 6
285347 for id, stableID := range paneStableIDs {
286- h.tab.panes[id].mu.Lock()
287- h.tab.panes[id].stableID = stableID
288- h.tab.panes[id].mu.Unlock()
348+ tb.panes[id].mu.Lock()
349+ tb.panes[id].stableID = stableID
350+ tb.panes[id].mu.Unlock()
289351 }
290- h.tab.mu.Unlock()
352+}
291353
292- operations := []struct {
354+func applyPaneLayoutSnapshotOperations(t *testing.T, h *paneRearrangeHarness) {
355+ t.Helper()
356+ for _, operation := range []struct {
293357 name string
294358 pane layout.PaneID
295359 direction layout.Direction
296360 }{
297361 {name: "consume singleton into vertical column", pane: "pane-1", direction: layout.Right},
298362 {name: "expel vertical member as singleton column", pane: "pane-3", direction: layout.Right},
299- }
300- for _, operation := range operations {
301- t.Run(operation.name, func(t *testing.T) {
302- err := (daemonActions{d: h.daemon}).Run(daemonActionRequest{
303- kind: daemonActionConsumeOrExpelPane, target: h.target(operation.pane), direction: operation.direction,
304- })
305- require.NoError(t, err)
363+ } {
364+ err := (daemonActions{d: h.daemon}).Run(daemonActionRequest{
365+ kind: daemonActionConsumeOrExpelPane, target: h.target(operation.pane), direction: operation.direction,
306366 })
367+ require.NoError(t, err, operation.name)
307368 }
369+}
308370
309- h.tab.mu.Lock()
310- wantTree := h.tab.tree.Clone()
311- h.tab.mu.Unlock()
312- require.Equal(t, layout.Horizontal, wantTree.Root.Dir)
313- require.Len(t, wantTree.Root.Children, 3)
314- require.Equal(t, layout.Vertical, wantTree.Root.Children[0].Dir)
315- require.Equal(t, layout.Leaf, wantTree.Root.Children[1].Kind)
316- require.Equal(t, layout.Stack, wantTree.Root.Children[2].Kind)
317- require.Equal(t, layout.PaneID("pane-5"), wantTree.Root.Children[2].Expanded)
318- require.Equal(t, layout.PaneID("pane-3"), wantTree.Focus)
319-
320- capture, ok := h.daemon.captureSnapshotState(h.session, 7)
321- require.True(t, ok)
322- publication, err := h.daemon.incrementalPublication(capture)
323- require.NoError(t, err)
324- objects := make(map[ports.SnapshotDigest][]byte, len(publication.Objects))
325- for _, object := range publication.Objects {
326- objects[object.Digest] = append([]byte(nil), object.Data...)
327- }
328- generation := ports.SnapshotGeneration{
329- IncarnationID: publication.IncarnationID,
330- Name: publication.Name,
331- Generation: publication.Generation,
332- Manifest: append([]byte(nil), publication.Manifest...),
333- Objects: objects,
334- }
335- decoded, err := sessionFromGeneration(generation)
336- require.NoError(t, err)
337- require.Len(t, decoded.Tabs, 1)
338- decodedTab := decoded.Tabs[0]
339- require.Equal(t, "tab-stable-rearranged", decodedTab.StableID)
340- require.Equal(t, layout.PaneID("pane-3"), decodedTab.Focus)
341- require.Equal(t, wantTree, decodedTab.Tree, "manifest codec must preserve the complete rearranged tree")
342- require.Equal(t, snapshotLayoutWeights(wantTree.Root), snapshotLayoutWeights(decodedTab.Tree.Root))
343- require.Equal(t, layout.PaneID("pane-5"), decodedTab.Tree.Root.Children[2].Expanded)
344- decodedStableIDs := make(map[layout.PaneID]string, len(decodedTab.Panes))
345- for _, restoredPane := range decodedTab.Panes {
346- decodedStableIDs[restoredPane.ID] = restoredPane.StableID
347- }
348- require.Equal(t, paneStableIDs, decodedStableIDs)
371+func assertConsumedAndExpelledColumns(t *testing.T, tree *layout.Tree) {
372+ t.Helper()
373+ require.NotNil(t, tree)
374+ require.NotNil(t, tree.Root)
375+ require.Equal(t, layout.Horizontal, tree.Root.Dir)
376+ require.Len(t, tree.Root.Children, 3)
377+ require.Equal(t, layout.Vertical, tree.Root.Children[0].Dir, "consumed panes must remain a vertical column")
378+ require.Equal(t, layout.Leaf, tree.Root.Children[1].Kind, "expelled pane must remain a singleton column")
379+ require.Equal(t, layout.Stack, tree.Root.Children[2].Kind, "stack column must remain present")
380+ require.Equal(t, layout.PaneID("pane-5"), tree.Root.Children[2].Expanded, "stack expansion must be preserved")
381+ require.Equal(t, layout.PaneID("pane-3"), tree.Focus, "expelled pane focus must be preserved")
382+}
349383
350- restorePTY := &paneRearrangePTY{}
351- restoreDaemon := newTestDaemon(t, newFactory(t, restorePTY), stubClock{})
352- restoredTabs, err := restoreDaemon.restoreSnapshotTabs(context.Background(), restoreDaemon.serveCtx, decoded)
353- require.NoError(t, err)
354- require.Len(t, restoredTabs, 1)
355- defer closeRestoredTabs(restoredTabs)
356- restoredTab := restoredTabs[0]
357- require.Equal(t, "tab-stable-rearranged", restoredTab.stableID)
358- require.Equal(t, wantTree, restoredTab.tree, "runtime restore must preserve the complete rearranged tree")
359- require.Equal(t, snapshotLayoutWeights(wantTree.Root), snapshotLayoutWeights(restoredTab.tree.Root))
360- require.Equal(t, layout.PaneID("pane-3"), restoredTab.tree.Focus)
361- require.Equal(t, layout.PaneID("pane-5"), restoredTab.tree.Root.Children[2].Expanded)
362- for id, stableID := range paneStableIDs {
363- require.Equal(t, stableID, restoredTab.panes[id].stableID, "pane %s stable ID", id)
384+func snapshotPaneStableIDs(panes []snapcodec.Pane) map[layout.PaneID]string {
385+ stableIDs := make(map[layout.PaneID]string, len(panes))
386+ for _, p := range panes {
387+ stableIDs[p.ID] = p.StableID
364388 }
389+ return stableIDs
365390 }
366391
367-func snapshotLayoutWeights(node *layout.Node) []float64 {
368- if node == nil {
369- return nil
370- }
371- weights := []float64{node.Weight}
372- for _, child := range node.Children {
373- weights = append(weights, snapshotLayoutWeights(child)...)
392+func captureRestoredPaneLayout(tb *tab) restoredPaneLayout {
393+ tb.mu.Lock()
394+ defer tb.mu.Unlock()
395+ stableIDs := make(map[layout.PaneID]string, len(tb.panes))
396+ for id, p := range tb.panes {
397+ p.mu.Lock()
398+ stableIDs[id] = p.stableID
399+ p.mu.Unlock()
374400 }
375- return weights
401+ return restoredPaneLayout{tree: tb.tree.Clone(), tabStableID: tb.stableID, paneStableIDs: stableIDs}
376402 }
377403
378404 func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) {
@@ -40,11 +40,7 @@ func (r *snapshotAcceptanceRepository) Publish(ctx context.Context, p ports.Snap
40 if r.publishErr != nil {40 if r.publishErr != nil {
41 return r.publishErr41 return r.publishErr
42 }42 }
43- objects := make(map[ports.SnapshotDigest][]byte, len(p.Objects))43+ r.generations[p.Name] = cloneAcceptanceGeneration(snapshotGeneration(p))
44- for _, object := range p.Objects {
45- objects[object.Digest] = append([]byte(nil), object.Data...)
46- }
47- r.generations[p.Name] = ports.SnapshotGeneration{IncarnationID: p.IncarnationID, Name: p.Name, Generation: p.Generation, ParentCheckpoint: p.ParentCheckpoint, Manifest: append([]byte(nil), p.Manifest...), Objects: objects}
48 found := false44 found := false
49 for _, name := range r.names {45 for _, name := range r.names {
50 found = found || name == p.Name46 found = found || name == p.Name
@@ -182,11 +178,9 @@ func acceptanceGeneration(t *testing.T, snapshot snapcodec.Session, generation u
182 manifest.Generation = generation178 manifest.Generation = generation
183 encoded, err := snapcodec.MarshalManifest(manifest)179 encoded, err := snapcodec.MarshalManifest(manifest)
184 require.NoError(t, err)180 require.NoError(t, err)
185- objects := make(map[ports.SnapshotDigest][]byte, len(publication.Objects))181+ publication.Generation = generation
186- for _, object := range publication.Objects {182+ publication.Manifest = encoded
187- objects[object.Digest] = append([]byte(nil), object.Data...)183+ return cloneAcceptanceGeneration(snapshotGeneration(publication))
188- }
189- return ports.SnapshotGeneration{IncarnationID: publication.IncarnationID, Name: snapshot.Name, Generation: generation, ParentCheckpoint: publication.ParentCheckpoint, Manifest: encoded, Objects: objects}
190 }184 }
191 185
192 func TestValidateRestoreSessionSnapshot(t *testing.T) {186 func TestValidateRestoreSessionSnapshot(t *testing.T) {
@@ -257,8 +251,82 @@ func TestRestoreIncrementalGenerationAcceptance(t *testing.T) {
257 require.Equal(t, uint64(10), repository.publishes[0].Generation, "a restored session must continue the concrete repository generation stream")251 require.Equal(t, uint64(10), repository.publishes[0].Generation, "a restored session must continue the concrete repository generation stream")
258 }252 }
259 253
260-func TestSnapshotCaptureRestorePreservesConsumedAndExpelledPaneLayout(t *testing.T) {254+type paneLayoutSnapshotFixture struct {
261- initial := &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Weight: 9, Children: []*layout.Node{255+ snapshot snapcodec.Session
256+ tree *layout.Tree
257+ tabStableID string
258+ paneStableIDs map[layout.PaneID]string
259+}
260+
261+type restoredPaneLayout struct {
262+ tree *layout.Tree
263+ tabStableID string
264+ paneStableIDs map[layout.PaneID]string
265+}
266+
267+func TestSnapshotCodecPreservesConsumedAndExpelledPaneLayout(t *testing.T) {
268+ fixture := captureConsumedAndExpelledPaneLayout(t)
269+ require.Len(t, fixture.snapshot.Tabs, 1)
270+ decodedTab := fixture.snapshot.Tabs[0]
271+
272+ require.Equal(t, fixture.tabStableID, decodedTab.StableID)
273+ require.Equal(t, layout.PaneID("pane-3"), decodedTab.Focus, "snapshot tab focus must be preserved")
274+ assertConsumedAndExpelledColumns(t, decodedTab.Tree)
275+ require.Equal(t, fixture.tree, decodedTab.Tree, "manifest codec must preserve the complete rearranged tree")
276+ require.Equal(t, fixture.paneStableIDs, snapshotPaneStableIDs(decodedTab.Panes), "manifest codec must preserve every pane stable ID")
277+}
278+
279+func TestSnapshotRuntimeRestorePreservesConsumedAndExpelledPaneLayout(t *testing.T) {
280+ fixture := captureConsumedAndExpelledPaneLayout(t)
281+ restoreDaemon := newTestDaemon(t, newFactory(t, &paneRearrangePTY{}), stubClock{})
282+ restoredTabs, err := restoreDaemon.restoreSnapshotTabs(context.Background(), restoreDaemon.serveCtx, fixture.snapshot)
283+ require.NoError(t, err)
284+ require.Len(t, restoredTabs, 1)
285+ defer closeRestoredTabs(restoredTabs)
286+
287+ restored := captureRestoredPaneLayout(restoredTabs[0])
288+ require.Equal(t, fixture.tabStableID, restored.tabStableID)
289+ assertConsumedAndExpelledColumns(t, restored.tree)
290+ require.Equal(t, fixture.tree, restored.tree, "runtime restore must preserve the complete rearranged tree")
291+ require.Equal(t, fixture.paneStableIDs, restored.paneStableIDs, "runtime restore must preserve every pane stable ID")
292+}
293+
294+func captureConsumedAndExpelledPaneLayout(t *testing.T) paneLayoutSnapshotFixture {
295+ t.Helper()
296+ h := newPaneRearrangeHarness(t, domain.Size{Cols: 200, Rows: 30}, paneLayoutSnapshotTree())
297+ h.session.createdAt = 77
298+ h.session.incarnation = domain.IncarnationID{7}
299+ tabStableID := "tab-stable-rearranged"
300+ paneStableIDs := map[layout.PaneID]string{
301+ "pane-1": "pane-stable-1",
302+ "pane-2": "pane-stable-2",
303+ "pane-3": "pane-stable-3",
304+ "pane-4": "pane-stable-4",
305+ "pane-5": "pane-stable-5",
306+ }
307+ setPaneLayoutSnapshotIDs(h.tab, tabStableID, paneStableIDs)
308+ applyPaneLayoutSnapshotOperations(t, h)
309+
310+ h.tab.mu.Lock()
311+ wantTree := h.tab.tree.Clone()
312+ h.tab.mu.Unlock()
313+ capture, ok := h.daemon.captureSnapshotState(h.session, 7)
314+ require.True(t, ok)
315+ publication, err := h.daemon.incrementalPublication(capture)
316+ require.NoError(t, err)
317+ decoded, err := sessionFromGeneration(snapshotGeneration(publication))
318+ require.NoError(t, err)
319+
320+ return paneLayoutSnapshotFixture{
321+ snapshot: decoded,
322+ tree: wantTree,
323+ tabStableID: tabStableID,
324+ paneStableIDs: paneStableIDs,
325+ }
326+}
327+
328+func paneLayoutSnapshotTree() *layout.Tree {
329+ return &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Weight: 9, Children: []*layout.Node{
262 {Kind: layout.Leaf, Leaf: "pane-1", Weight: 40},330 {Kind: layout.Leaf, Leaf: "pane-1", Weight: 40},
263 {Kind: layout.Split, Dir: layout.Vertical, Weight: 80, Children: []*layout.Node{331 {Kind: layout.Split, Dir: layout.Vertical, Weight: 80, Children: []*layout.Node{
264 {Kind: layout.Leaf, Leaf: "pane-2", Weight: 10},332 {Kind: layout.Leaf, Leaf: "pane-2", Weight: 10},
@@ -269,110 +337,68 @@ func TestSnapshotCaptureRestorePreservesConsumedAndExpelledPaneLayout(t *testing
269 {Kind: layout.Leaf, Leaf: "pane-5", Weight: 3},337 {Kind: layout.Leaf, Leaf: "pane-5", Weight: 3},
270 }},338 }},
271 }}, Focus: "pane-4"}339 }}, Focus: "pane-4"}
272- h := newPaneRearrangeHarness(t, domain.Size{Cols: 200, Rows: 30}, initial)340+}
273- h.session.createdAt = 77341+
274- h.session.incarnation = domain.IncarnationID{7}342+func setPaneLayoutSnapshotIDs(tb *tab, tabStableID string, paneStableIDs map[layout.PaneID]string) {
275- h.tab.mu.Lock()343+ tb.mu.Lock()
276- h.tab.stableID = "tab-stable-rearranged"344+ defer tb.mu.Unlock()
277- h.tab.nextPaneID = 6345+ tb.stableID = tabStableID
278- paneStableIDs := map[layout.PaneID]string{346+ tb.nextPaneID = 6
279- "pane-1": "pane-stable-1",
280- "pane-2": "pane-stable-2",
281- "pane-3": "pane-stable-3",
282- "pane-4": "pane-stable-4",
283- "pane-5": "pane-stable-5",
284- }
285 for id, stableID := range paneStableIDs {347 for id, stableID := range paneStableIDs {
286- h.tab.panes[id].mu.Lock()348+ tb.panes[id].mu.Lock()
287- h.tab.panes[id].stableID = stableID349+ tb.panes[id].stableID = stableID
288- h.tab.panes[id].mu.Unlock()350+ tb.panes[id].mu.Unlock()
289 }351 }
290- h.tab.mu.Unlock()352+}
291 353
292- operations := []struct {354+func applyPaneLayoutSnapshotOperations(t *testing.T, h *paneRearrangeHarness) {
355+ t.Helper()
356+ for _, operation := range []struct {
293 name string357 name string
294 pane layout.PaneID358 pane layout.PaneID
295 direction layout.Direction359 direction layout.Direction
296 }{360 }{
297 {name: "consume singleton into vertical column", pane: "pane-1", direction: layout.Right},361 {name: "consume singleton into vertical column", pane: "pane-1", direction: layout.Right},
298 {name: "expel vertical member as singleton column", pane: "pane-3", direction: layout.Right},362 {name: "expel vertical member as singleton column", pane: "pane-3", direction: layout.Right},
299- }363+ } {
300- for _, operation := range operations {364+ err := (daemonActions{d: h.daemon}).Run(daemonActionRequest{
301- t.Run(operation.name, func(t *testing.T) {365+ kind: daemonActionConsumeOrExpelPane, target: h.target(operation.pane), direction: operation.direction,
302- err := (daemonActions{d: h.daemon}).Run(daemonActionRequest{
303- kind: daemonActionConsumeOrExpelPane, target: h.target(operation.pane), direction: operation.direction,
304- })
305- require.NoError(t, err)
306 })366 })
367+ require.NoError(t, err, operation.name)
307 }368 }
369+}
308 370
309- h.tab.mu.Lock()371+func assertConsumedAndExpelledColumns(t *testing.T, tree *layout.Tree) {
310- wantTree := h.tab.tree.Clone()372+ t.Helper()
311- h.tab.mu.Unlock()373+ require.NotNil(t, tree)
312- require.Equal(t, layout.Horizontal, wantTree.Root.Dir)374+ require.NotNil(t, tree.Root)
313- require.Len(t, wantTree.Root.Children, 3)375+ require.Equal(t, layout.Horizontal, tree.Root.Dir)
314- require.Equal(t, layout.Vertical, wantTree.Root.Children[0].Dir)376+ require.Len(t, tree.Root.Children, 3)
315- require.Equal(t, layout.Leaf, wantTree.Root.Children[1].Kind)377+ require.Equal(t, layout.Vertical, tree.Root.Children[0].Dir, "consumed panes must remain a vertical column")
316- require.Equal(t, layout.Stack, wantTree.Root.Children[2].Kind)378+ require.Equal(t, layout.Leaf, tree.Root.Children[1].Kind, "expelled pane must remain a singleton column")
317- require.Equal(t, layout.PaneID("pane-5"), wantTree.Root.Children[2].Expanded)379+ require.Equal(t, layout.Stack, tree.Root.Children[2].Kind, "stack column must remain present")
318- require.Equal(t, layout.PaneID("pane-3"), wantTree.Focus)380+ require.Equal(t, layout.PaneID("pane-5"), tree.Root.Children[2].Expanded, "stack expansion must be preserved")
319-381+ require.Equal(t, layout.PaneID("pane-3"), tree.Focus, "expelled pane focus must be preserved")
320- capture, ok := h.daemon.captureSnapshotState(h.session, 7)382+}
321- require.True(t, ok)
322- publication, err := h.daemon.incrementalPublication(capture)
323- require.NoError(t, err)
324- objects := make(map[ports.SnapshotDigest][]byte, len(publication.Objects))
325- for _, object := range publication.Objects {
326- objects[object.Digest] = append([]byte(nil), object.Data...)
327- }
328- generation := ports.SnapshotGeneration{
329- IncarnationID: publication.IncarnationID,
330- Name: publication.Name,
331- Generation: publication.Generation,
332- Manifest: append([]byte(nil), publication.Manifest...),
333- Objects: objects,
334- }
335- decoded, err := sessionFromGeneration(generation)
336- require.NoError(t, err)
337- require.Len(t, decoded.Tabs, 1)
338- decodedTab := decoded.Tabs[0]
339- require.Equal(t, "tab-stable-rearranged", decodedTab.StableID)
340- require.Equal(t, layout.PaneID("pane-3"), decodedTab.Focus)
341- require.Equal(t, wantTree, decodedTab.Tree, "manifest codec must preserve the complete rearranged tree")
342- require.Equal(t, snapshotLayoutWeights(wantTree.Root), snapshotLayoutWeights(decodedTab.Tree.Root))
343- require.Equal(t, layout.PaneID("pane-5"), decodedTab.Tree.Root.Children[2].Expanded)
344- decodedStableIDs := make(map[layout.PaneID]string, len(decodedTab.Panes))
345- for _, restoredPane := range decodedTab.Panes {
346- decodedStableIDs[restoredPane.ID] = restoredPane.StableID
347- }
348- require.Equal(t, paneStableIDs, decodedStableIDs)
349 383
350- restorePTY := &paneRearrangePTY{}384+func snapshotPaneStableIDs(panes []snapcodec.Pane) map[layout.PaneID]string {
351- restoreDaemon := newTestDaemon(t, newFactory(t, restorePTY), stubClock{})385+ stableIDs := make(map[layout.PaneID]string, len(panes))
352- restoredTabs, err := restoreDaemon.restoreSnapshotTabs(context.Background(), restoreDaemon.serveCtx, decoded)386+ for _, p := range panes {
353- require.NoError(t, err)387+ stableIDs[p.ID] = p.StableID
354- require.Len(t, restoredTabs, 1)
355- defer closeRestoredTabs(restoredTabs)
356- restoredTab := restoredTabs[0]
357- require.Equal(t, "tab-stable-rearranged", restoredTab.stableID)
358- require.Equal(t, wantTree, restoredTab.tree, "runtime restore must preserve the complete rearranged tree")
359- require.Equal(t, snapshotLayoutWeights(wantTree.Root), snapshotLayoutWeights(restoredTab.tree.Root))
360- require.Equal(t, layout.PaneID("pane-3"), restoredTab.tree.Focus)
361- require.Equal(t, layout.PaneID("pane-5"), restoredTab.tree.Root.Children[2].Expanded)
362- for id, stableID := range paneStableIDs {
363- require.Equal(t, stableID, restoredTab.panes[id].stableID, "pane %s stable ID", id)
364 }388 }
389+ return stableIDs
365 }390 }
366 391
367-func snapshotLayoutWeights(node *layout.Node) []float64 {392+func captureRestoredPaneLayout(tb *tab) restoredPaneLayout {
368- if node == nil {393+ tb.mu.Lock()
369- return nil394+ defer tb.mu.Unlock()
370- }395+ stableIDs := make(map[layout.PaneID]string, len(tb.panes))
371- weights := []float64{node.Weight}396+ for id, p := range tb.panes {
372- for _, child := range node.Children {397+ p.mu.Lock()
373- weights = append(weights, snapshotLayoutWeights(child)...)398+ stableIDs[id] = p.stableID
399+ p.mu.Unlock()
374 }400 }
375- return weights401+ return restoredPaneLayout{tree: tb.tree.Clone(), tabStableID: tb.stableID, paneStableIDs: stableIDs}
376 }402 }
377 403
378 func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) {404 func TestRestoredSessionMetadataUpdatePreservesCheckpointLineage(t *testing.T) {

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