refactor/kvconf 3421f16

session_test.go RawHistoryFolder

1package visual
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"io"
8	"testing"
9	"time"
10
11	"github.com/bnema/neferafk/internal/adapters/visualproc"
12	"github.com/bnema/neferafk/internal/ports"
13	"github.com/stretchr/testify/mock"
14	"github.com/stretchr/testify/require"
15)
16
17type harness struct {
18	cmdW   *io.PipeWriter
19	events *io.PipeReader
20	done   chan error
21	cfgs   chan lockConfig
22	l      *mockworkerLauncher
23	lock   *mocklockRunner
24	fade   *mockfadeRunner
25}
26
27// newHarness runs a session over real pipes; RunLock is a mock that publishes
28// its config and returns nil only once Unlock fires (or ctx ends).
29func newHarness(t *testing.T) *harness { return newHarnessRun(t, nil) }
30
31// newHarnessRun is newHarness with an optional replacement RunLock body.
32func newHarnessRun(t *testing.T, runLock func(context.Context, lockConfig) error) *harness {
33	t.Helper()
34	cmdR, cmdW := io.Pipe()
35	evR, evW := io.Pipe()
36	h := &harness{cmdW: cmdW, events: evR, done: make(chan error, 1), cfgs: make(chan lockConfig, 1),
37		l: newMockworkerLauncher(t), lock: newMocklockRunner(t), fade: newMockfadeRunner(t)}
38	if runLock == nil {
39		runLock = func(ctx context.Context, cfg lockConfig) error {
40			h.cfgs <- cfg
41			select {
42			case <-cfg.Unlock:
43				return nil
44			case <-ctx.Done():
45				return ctx.Err()
46			}
47		}
48	}
49	h.lock.EXPECT().RunLock(mock.Anything, mock.Anything).RunAndReturn(runLock).Maybe()
50	s := &session{log: testLog(), cmds: cmdR, events: evW, locker: h.lock, fader: h.fade, launcher: h.l}
51	go func() { h.done <- s.run(context.Background()) }()
52	t.Cleanup(func() { cmdW.Close(); evR.Close() })
53	return h
54}
55
56func (h *harness) sendLock(t *testing.T) {
57	require.NoError(t, visualproc.WriteCommand(h.cmdW, ports.VisualLock{Generation: gen, Auth: boot()}))
58}
59
60func (h *harness) event(t *testing.T) ports.VisualEvent {
61	t.Helper()
62	ch := make(chan ports.VisualEvent, 1)
63	go func() { ev, _ := visualproc.ReadEvent(h.events); ch <- ev }()
64	return recv(t, ch)
65}
66
67func TestLockReleasedOnlyAfterAuthSuccess(t *testing.T) {
68	h := newHarness(t)
69	w := expectWorker(t, h.l)
70	h.sendLock(t)
71	cfg := recv(t, h.cfgs)
72	w.bootstrap(t)
73	w.ready(t)
74	cfg.OnLocked()
75	require.Equal(t, ports.LockConfirmed{Generation: gen}, h.event(t))
76
77	cfg.OnSubmit([]byte("bad"))
78	f := w.attempt(t)
79	w.result(t, f.Attempt, ports.AuthDenied)
80	require.Equal(t, lockFailed, recvFailed(t, cfg.Status))
81	select {
82	case err := <-h.done:
83		t.Fatalf("session ended after denial: %v", err)
84	case <-time.After(100 * time.Millisecond):
85	}
86
87	cfg.OnSubmit([]byte("good"))
88	f = w.attempt(t)
89	w.result(t, f.Attempt, ports.AuthSuccess)
90	require.Equal(t, ports.LockReleased{Generation: gen}, h.event(t))
91	require.NoError(t, recv(t, h.done))
92	recv(t, w.killed) // worker reaped with the lock
93}
94
95func recvFailed(t *testing.T, c <-chan lockStatus) lockStatus {
96	for {
97		if s := recv(t, c); s == lockFailed {
98			return s
99		}
100	}
101}
102
103func TestWorkerCrashKeepsLock(t *testing.T) {
104	h := newHarness(t)
105	w := expectWorker(t, h.l)
106	h.sendLock(t)
107	cfg := recv(t, h.cfgs)
108	w.bootstrap(t)
109	w.ready(t)
110	cfg.OnLocked()
111	h.event(t)
112	cfg.OnSubmit([]byte("x"))
113	w.attempt(t)
114	w.toVisual.Close()
115	recv(t, w.killed)
116	recvFailed(t, cfg.Status)
117	select {
118	case err := <-h.done:
119		t.Fatalf("worker crash ended the lock: %v", err)
120	case <-time.After(100 * time.Millisecond):
121	}
122}
123
124func TestDaemonEOFKeepsLockUntilUnlock(t *testing.T) {
125	h := newHarness(t)
126	w := expectWorker(t, h.l)
127	h.sendLock(t)
128	cfg := recv(t, h.cfgs)
129	w.bootstrap(t)
130	w.ready(t)
131	cfg.OnLocked()
132	h.event(t)
133	h.cmdW.Close() // daemon gone
134	select {
135	case err := <-h.done:
136		t.Fatalf("EOF dropped an active lock: %v", err)
137	case <-time.After(100 * time.Millisecond):
138	}
139	cfg.OnSubmit([]byte("good"))
140	f := w.attempt(t)
141	w.result(t, f.Attempt, ports.AuthSuccess)
142	require.Equal(t, ports.LockReleased{Generation: gen}, h.event(t))
143	require.NoError(t, recv(t, h.done))
144}
145
146func TestFadeOnlyExitsOnEOF(t *testing.T) {
147	h := newHarness(t)
148	started := make(chan *fadeModel, 1)
149	h.fade.EXPECT().RunFade(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, m *fadeModel) error {
150		started <- m
151		<-ctx.Done()
152		return nil
153	}).Once()
154	require.NoError(t, visualproc.WriteCommand(h.cmdW, ports.VisualFade{Black: true, Duration: time.Second}))
155	m := recv(t, started)
156	require.Equal(t, fadeCmd{black: true, dur: time.Second}, recv(t, m.cmds))
157	h.cmdW.Close()
158	require.NoError(t, recv(t, h.done))
159}
160
161func TestRevealWithoutFadeExitsWithoutSpawning(t *testing.T) {
162	h := newHarness(t)
163	require.NoError(t, visualproc.WriteCommand(h.cmdW, ports.VisualFade{Black: false}))
164	require.NoError(t, recv(t, h.done))
165}
166
167func TestFinishedRevealExits(t *testing.T) {
168	h := newHarness(t)
169	release := make(chan struct{})
170	h.fade.EXPECT().RunFade(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, m *fadeModel) error {
171		<-release
172		return nil
173	}).Once()
174	require.NoError(t, visualproc.WriteCommand(h.cmdW, ports.VisualFade{Black: true}))
175	close(release)
176	require.NoError(t, recv(t, h.done))
177}
178
179// noEvent fails if the session emitted anything on the event pipe.
180func (h *harness) noEvent(t *testing.T) {
181	t.Helper()
182	got := make(chan ports.VisualEvent, 1)
183	go func() {
184		ev, err := visualproc.ReadEvent(h.events)
185		if err == nil {
186			got <- ev
187		}
188	}()
189	select {
190	case ev := <-got:
191		t.Fatalf("unexpected event %#v", ev)
192	case <-time.After(100 * time.Millisecond):
193	}
194}
195
196func TestRunLockNilWithoutAuthSuccessNeverReleases(t *testing.T) {
197	h := newHarnessRun(t, func(context.Context, lockConfig) error { return nil })
198	w := expectWorker(t, h.l)
199	h.sendLock(t)
200	w.bootstrap(t)
201	err := recv(t, h.done)
202	require.ErrorIs(t, err, errLockWithoutAuth)
203	h.noEvent(t) // no LockReleased
204}
205
206func TestRunLockErrorNeverReleases(t *testing.T) {
207	for name, runErr := range map[string]error{
208		"finished": errLockFinished,
209		"wrapped":  fmt.Errorf("compositor: %w", errLockFinished),
210		"other":    errors.New("boom"),
211	} {
212		t.Run(name, func(t *testing.T) {
213			h := newHarnessRun(t, func(context.Context, lockConfig) error { return runErr })
214			w := expectWorker(t, h.l)
215			h.sendLock(t)
216			w.bootstrap(t)
217			require.ErrorIs(t, recv(t, h.done), runErr)
218			h.noEvent(t)
219		})
220	}
221}

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 2

Tags 9

↑↓ moveenter openesc close