v0.1.0 07cdc18

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

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