session_core_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/protocol"
9	"github.com/stretchr/testify/require"
10)
11
12func clearAttachmentsForTest(sess *session) {
13	for _, ac := range sess.snapshotAttachments() {
14		sess.unregisterAttachment(ac)
15	}
16}
17
18func clearAttachmentsForTestLocked(sess *session) {
19	for _, ac := range sess.snapshotAttachmentsLocked() {
20		sess.unregisterAttachmentLocked(ac)
21	}
22}
23
24func TestSessionCorePreservesLocalIdentityAndPromotedMutex(t *testing.T) {
25	sess := &session{}
26	sess.id = domain.SessionID("local")
27	sess.name = "work"
28	sess.tabs = []*tab{{name: "shell"}}
29	ac := &attachedClient{}
30
31	core := sess.core()
32	require.Same(t, &sess.sessionCore, core)
33	require.Equal(t, sessionCapabilities{}, core.caps, "zero-value local sessions must retain all local capabilities")
34
35	core.mu.Lock()
36	require.False(t, sess.mu.TryLock(), "sessionCore.mu must be the mutex promoted as session.mu")
37	core.registerAttachmentLocked(ac)
38	require.Equal(t, true, sess.attachmentRegisteredLocked(ac))
39	sess.tabs = append(sess.tabs, &tab{name: "logs"})
40	require.Len(t, sess.tabs, 2)
41	core.mu.Unlock()
42
43	require.True(t, sess.mu.TryLock())
44	sess.mu.Unlock()
45}
46
47func TestSessionSnapshotViewUsesPromotedMutexForLocalTabAndRoleState(t *testing.T) {
48	sess := &session{
49		sessionCore: sessionCore{id: domain.SessionID("local"), name: "work"},
50		tabs:        []*tab{{stableID: "tab-shell", name: "shell"}},
51	}
52	ac := &attachedClient{}
53
54	// Mutate local-only tab state and shared attachment role state under the
55	// promoted core mutex. snapshotView must observe the same mutex before it
56	// can publish either field.
57	sess.core().mu.Lock()
58	sess.tabs = append(sess.tabs, &tab{stableID: "tab-logs", name: "logs"})
59	sess.registerAttachmentLocked(ac)
60	sess.activateAttachmentViewLocked(ac, 1)
61
62	preCall := make(chan struct{})
63	allowCall := make(chan struct{})
64	snapshotted := make(chan sessionView, 1)
65	go func() {
66		// The barrier places the goroutine immediately before snapshotView, so
67		// the lock is definitely held before the call can begin.
68		close(preCall)
69		<-allowCall
70		snapshotted <- sess.snapshotView(viewOptions{})
71	}()
72	awaitTestCompletion(t, preCall, "snapshot goroutine did not reach the pre-call barrier")
73	close(allowCall)
74
75	// Give the released call a bounded opportunity to acquire the mutex; it
76	// must remain blocked until the owner releases it below.
77	timer := time.NewTimer(50 * time.Millisecond)
78	defer timer.Stop()
79	select {
80	case <-snapshotted:
81		t.Fatal("snapshotView completed while sessionCore.mu guarded local session state")
82	case <-timer.C:
83	}
84	require.Equal(t, true, sess.attachmentRegisteredLocked(ac))
85	sess.core().mu.Unlock()
86
87	view := awaitTestValue(t, snapshotted, "snapshotView did not complete after sessionCore.mu was released")
88	require.True(t, view.attached)
89	require.Equal(t, 2, view.tabCount)
90	require.Equal(t, 0, view.defaultTab)
91	require.Equal(t, 1, testAttachmentTabIndex(sess))
92}
93
94func TestLocalCreateThenKillRemovesLiveRegistryEntry(t *testing.T) {
95	pty, releasePTY := newBlockingPTY(t)
96	defer releasePTY()
97	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
98
99	sess, err := createSessionForTest(d, "0", true, "/tmp", defaultSize, terminalEnv{}, nil)
100	require.NoError(t, err)
101	d.mu.Lock()
102	registered := d.sessions[sess.id]
103	d.mu.Unlock()
104	require.Same(t, sess, registered)
105
106	require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
107	d.mu.Lock()
108	_, live := d.sessions[sess.id]
109	d.mu.Unlock()
110	require.False(t, live, "killed local session must not remain in the live registry")
111	d.sessWg.Wait()
112}
113
114func TestAttachmentSessionRegistryUsesExactIdentity(t *testing.T) {
115	d := &Daemon{sessions: make(map[domain.SessionID]*session)}
116	first := &session{}
117	first.id = domain.SessionID("same")
118	second := &session{}
119	second.id = first.id
120
121	require.True(t, d.registerSessionLocked(first))
122	require.False(t, d.registerSessionLocked(first), "duplicate registration must be rejected")
123	require.False(t, d.registerSessionLocked(second), "a different pointer with the same ID must be rejected")
124	require.False(t, d.unregisterSessionLocked(second), "unregister must not remove a replacement pointer")
125	require.Same(t, first, d.sessions[first.id])
126	require.True(t, d.unregisterSessionLocked(first))
127	require.NotContains(t, d.sessions, first.id)
128	require.False(t, d.unregisterSessionLocked(first))
129}
130
131func TestSessionCoreLockOrderUsesImmutableIDs(t *testing.T) {
132	first := &session{}
133	first.id = domain.SessionID("a")
134	second := &session{}
135	second.id = domain.SessionID("z")
136
137	unlock := lockAttachmentSessions(second, first)
138	require.False(t, first.core().mu.TryLock())
139	require.False(t, second.core().mu.TryLock())
140	unlock()
141
142	require.True(t, first.core().mu.TryLock())
143	first.core().mu.Unlock()
144	require.True(t, second.core().mu.TryLock())
145	second.core().mu.Unlock()
146}
147
148func mustLocalSession(t *testing.T, entry *session) *session {
149	t.Helper()
150	require.NotNil(t, entry)
151	return entry
152}

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