v0.2.0 80c61e3

move_cleanup_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/require"
7
8	"github.com/bnema/vev/internal/domain"
9	scopy "github.com/bnema/vev/internal/usecase/copy"
10)
11
12func TestDetachMoveAttachmentsUsesStableSnapshotOrder(t *testing.T) {
13	first := &attachedClient{clientID: [16]byte{1}}
14	second := &attachedClient{clientID: [16]byte{2}}
15	sess := &session{sessionCore: sessionCore{attachments: map[*attachedClient]struct{}{second: {}, first: {}}}}
16	frozen := freezeAttachmentEffectGates(first, second)
17	require.True(t, frozen.acquired)
18	defer frozen.unfreeze()
19
20	sess.mu.Lock()
21	retired := detachMoveAttachmentsLocked(sess, nil)
22	sess.mu.Unlock()
23
24	require.Len(t, retired, 2)
25	require.Same(t, first, retired[0].ac)
26	require.Same(t, second, retired[1].ac)
27}
28
29func TestMovePaneReleasesResizeFencesBeforeAttachmentCleanup(t *testing.T) {
30	movedPTY, releaseMoved := newBlockingPTY(t)
31	remainingPTY, releaseRemaining := newBlockingPTY(t)
32	destinationPTY, releaseDestination := newBlockingPTY(t)
33	defer releaseMoved()
34	defer releaseRemaining()
35	defer releaseDestination()
36
37	d, source, client, _ := newManualSessionWithPTYs(t, movedPTY, remainingPTY)
38	require.NotNil(t, d.attachCoordinator(source, nil, client, true))
39	sourceTab := source.tabs[0]
40	sourceTab.stableID = "source-tab"
41	moved := sourceTab.focusedPane()
42	client.sendMu.Lock()
43	client.captureFrames = map[*pane]capturedPaneRenderState{moved: {}}
44	client.sendMu.Unlock()
45
46	destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
47	destinationTab := destination.tabs[0]
48	publishTiledPaneOwners(destination, destinationTab)
49	d.mu.Lock()
50	d.sessions[destination.id] = destination
51	d.mu.Unlock()
52
53	moved.mu.Lock()
54	owner := moved.effectLeaseLocked()
55	moved.mu.Unlock()
56	members := []resizeMember{{session: source, tab: sourceTab, pane: moved, owner: owner}}
57	rc := source.renderCoordinator()
58	require.NotNil(t, rc)
59	lease := rc.attachmentLease(client)
60	epoch := rc.recordResizeRequestForLease(client.size, client, lease)
61	ticket, admitted := beginAttachmentLeaseEffect(source, client, lease)
62	require.True(t, admitted)
63	defer ticket.End()
64
65	sendLocked := make(chan struct{})
66	allowResizeFence := make(chan struct{})
67	d.afterResizeCommitSendLocked = func() {
68		close(sendLocked)
69		<-allowResizeFence
70	}
71	defer func() { d.afterResizeCommitSendLocked = nil }()
72	resizeDone := make(chan bool, 1)
73	go func() {
74		resizeDone <- d.publishResizeCommit(members, source, client, lease, epoch, ticket, client.size)
75	}()
76	awaitTestCompletion(t, sendLocked, "resize commit did not acquire sendMu")
77
78	insideMoveCommit := make(chan struct{})
79	allowMoveCommit := make(chan struct{})
80	d.beforeMovePaneCommit = func() {
81		close(insideMoveCommit)
82		<-allowMoveCommit
83	}
84	defer func() { d.beforeMovePaneCommit = nil }()
85	moveDone := make(chan error, 1)
86	go func() {
87		moveDone <- d.movePane(movePaneRequest{
88			Source:           moveSessionLocator{ID: source.id, Incarnation: source.incarnation},
89			SourceTabID:      domain.TabStableID(sourceTab.stableID),
90			SourcePaneID:     domain.PaneStableID(moved.stableID),
91			Destination:      moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},
92			DestinationTabID: domain.TabStableID(destinationTab.stableID),
93		})
94	}()
95	awaitTestCompletion(t, insideMoveCommit, "move did not reach its commit section")
96
97	// Resize now owns sendMu and waits for the moved pane fence. The move may
98	// finish only if it releases every resize fence before capture cleanup tries
99	// to acquire sendMu.
100	close(allowResizeFence)
101	close(allowMoveCommit)
102	require.NoError(t, awaitTestValue(t, moveDone, "move deadlocked against resize commit"))
103	require.False(t, awaitTestValue(t, resizeDone, "resize commit did not drop stale owner"))
104}
105
106func TestMovePaneCleanupUsesCommitPointSourceAttachmentToken(t *testing.T) {
107	movedPTY, releaseMoved := newBlockingPTY(t)
108	remainingPTY, releaseRemaining := newBlockingPTY(t)
109	destinationPTY, releaseDestination := newBlockingPTY(t)
110	defer releaseMoved()
111	defer releaseRemaining()
112	defer releaseDestination()
113
114	d, source, displaced, _ := newManualSessionWithPTYs(t, movedPTY, remainingPTY)
115	sourceTab := source.tabs[0]
116	remainingTab := source.tabs[1]
117	sourceTab.stableID = "source-tab"
118	remainingTab.stableID = "remaining-tab"
119	moved := sourceTab.focusedPane()
120	require.NotNil(t, d.attachCoordinator(source, nil, displaced, true))
121
122	destination := &session{sessionCore: sessionCore{id: "destination", name: "destination", ephemeral: true}, tabs: []*tab{newTabWithStableID("destination-tab", "destination-pane", destinationPTY, domain.Size{Cols: 80, Rows: 23})}}
123	destinationTab := destination.tabs[0]
124	publishTiledPaneOwners(destination, destinationTab)
125	d.mu.Lock()
126	d.sessions[destination.id] = destination
127	d.mu.Unlock()
128
129	// Both clients carry state tied specifically to the pane. The existing
130	// attachment and the concurrently registered attachment remain independent;
131	// moving one exact attachment must not clean the other.
132	displaced.overlays.copyMu.Lock()
133	displaced.overlays.copyPane = moved
134	displaced.overlays.copyMode = &scopy.Mode{}
135	displaced.overlays.copyMu.Unlock()
136	displaced.sendMu.Lock()
137	displaced.captureFrames = map[*pane]capturedPaneRenderState{moved: {}}
138	displaced.sendMu.Unlock()
139
140	replacementTransport, _ := newCapturingTransport(t)
141	replacement := &attachedClient{
142		tr: replacementTransport, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24},
143	}
144	replacement.initOverlays()
145	replacement.setSession(source)
146	replacement.overlays.copyMu.Lock()
147	replacement.overlays.copyPane = moved
148	replacement.overlays.copyMode = &scopy.Mode{}
149	replacement.overlays.copyMu.Unlock()
150	replacement.sendMu.Lock()
151	replacement.captureFrames = map[*pane]capturedPaneRenderState{moved: {}}
152	replacement.sendMu.Unlock()
153
154	// Keep the source layout fence occupied while the move has captured its
155	// pre-fence source client. Replacing the client at this barrier reproduces
156	// the stale cleanup race without sleeps.
157	source.layoutApplyMu.Lock()
158	snapshot := make(chan struct{})
159	releaseSnapshot := make(chan struct{})
160	d.afterMovePaneSourceSnapshot = func() {
161		close(snapshot)
162		<-releaseSnapshot
163	}
164	defer func() { d.afterMovePaneSourceSnapshot = nil }()
165
166	moveDone := make(chan error, 1)
167	go func() {
168		moveDone <- d.movePane(movePaneRequest{
169			Source:           moveSessionLocator{ID: source.id, Incarnation: source.incarnation},
170			SourceTabID:      domain.TabStableID(sourceTab.stableID),
171			SourcePaneID:     domain.PaneStableID(moved.stableID),
172			Destination:      moveSessionLocator{ID: destination.id, Incarnation: destination.incarnation},
173			DestinationTabID: domain.TabStableID(destinationTab.stableID),
174		})
175	}()
176	awaitTestCompletion(t, snapshot, "move did not capture its pre-fence source attachment")
177
178	source.mu.Lock()
179	source.registerAttachmentLocked(replacement)
180	source.mu.Unlock()
181	close(releaseSnapshot)
182	source.layoutApplyMu.Unlock()
183	require.NoError(t, awaitTestValue(t, moveDone, "move did not finish after source fence release"))
184
185	displaced.overlays.copyMu.Lock()
186	require.Nil(t, displaced.overlays.copyMode, "moved attachment copy mode was not cleaned")
187	require.Nil(t, displaced.overlays.copyPane)
188	displaced.overlays.copyMu.Unlock()
189	displaced.sendMu.Lock()
190	_, displacedCaptured := displaced.captureFrames[moved]
191	displaced.sendMu.Unlock()
192	require.False(t, displacedCaptured, "moved attachment capture was not pruned")
193
194	replacement.overlays.copyMu.Lock()
195	require.NotNil(t, replacement.overlays.copyMode, "independent attachment copy mode was cleaned")
196	require.Same(t, moved, replacement.overlays.copyPane)
197	replacement.overlays.copyMu.Unlock()
198	replacement.sendMu.Lock()
199	_, replacementCaptured := replacement.captureFrames[moved]
200	replacement.sendMu.Unlock()
201	require.True(t, replacementCaptured, "independent attachment capture was pruned")
202}

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