readiness_test.go RawHistoryFolder

1package systemviews
2
3// P4.2 readiness tests: the shell loading state clears after the initial
4// mount and action binding, never gated on async history-data completion.
5// Fatal mount/bind failures report to the shell while preserving the
6// original error.
7
8import (
9	"context"
10	"errors"
11	"testing"
12	"time"
13
14	"github.com/bnema/dumber/internal/domain/entity"
15	"github.com/stretchr/testify/require"
16)
17
18// signalingDOM records readiness signals on top of the recording mount.
19type signalingDOM struct {
20	recordingDOM
21	ready  bool
22	failed string
23}
24
25func (d *signalingDOM) SignalReady() error {
26	d.mu.Lock()
27	defer d.mu.Unlock()
28	d.ready = true
29	return nil
30}
31
32func (d *signalingDOM) SignalError(message string) error {
33	d.mu.Lock()
34	defer d.mu.Unlock()
35	d.failed = message
36	return nil
37}
38
39func (d *signalingDOM) isReady() bool {
40	d.mu.Lock()
41	defer d.mu.Unlock()
42	return d.ready
43}
44
45func (d *signalingDOM) failure() string {
46	d.mu.Lock()
47	defer d.mu.Unlock()
48	return d.failed
49}
50
51// signalingActionDOM binds actions and signals readiness.
52type signalingActionDOM struct {
53	signalingDOM
54	handler DOMActionHandler
55}
56
57func (d *signalingActionDOM) BindActions(handler DOMActionHandler) error {
58	d.handler = handler
59	return nil
60}
61
62func TestReadinessSignaledAfterMountAndBind(t *testing.T) {
63	t.Parallel()
64
65	dom := &signalingActionDOM{}
66	app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
67	require.NoError(t, app.Run())
68	require.True(t, dom.isReady(), "readiness must fire after mount and binding")
69	require.Empty(t, dom.failure())
70}
71
72func TestReadinessMountFailureSignalsError(t *testing.T) {
73	t.Parallel()
74
75	dom := &failingMountSignalingDOM{err: errors.New("mount blew up")}
76	app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
77	err := app.Run()
78	require.ErrorIs(t, err, dom.err, "original mount error must propagate")
79	require.Contains(t, dom.failure(), "mount blew up")
80	require.False(t, dom.isReady())
81}
82
83// failingMountSignalingDOM fails Mount while recording signals.
84type failingMountSignalingDOM struct {
85	signalingDOM
86	err error
87}
88
89func (d *failingMountSignalingDOM) Mount(string) error {
90	return d.err
91}
92
93func TestReadinessBindFailureSignalsError(t *testing.T) {
94	t.Parallel()
95
96	dom := &failingBindSignalingDOM{err: errors.New("bind blew up")}
97	app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
98	err := app.Run()
99	require.ErrorIs(t, err, dom.err, "original bind error must propagate")
100	require.Contains(t, dom.failure(), "bind blew up")
101	require.False(t, dom.isReady())
102}
103
104// failingBindSignalingDOM fails BindActions while recording signals.
105type failingBindSignalingDOM struct {
106	signalingDOM
107	err error
108}
109
110func (d *failingBindSignalingDOM) BindActions(DOMActionHandler) error {
111	return d.err
112}
113
114func TestReadinessWithoutSignalerKeepsLegacyBehavior(t *testing.T) {
115	t.Parallel()
116
117	dom := &recordingDOM{}
118	app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
119	require.NoError(t, app.Run())
120	require.True(t, dom.Mounted())
121}
122
123func TestReadinessSignalFailurePropagates(t *testing.T) {
124	t.Parallel()
125
126	dom := &failingReadySignalingDOM{err: errors.New("signal blew up")}
127	app := NewApp(Dependencies{DOM: dom, LocationURI: "dumb://history"})
128	require.ErrorIs(t, app.Run(), dom.err)
129}
130
131// failingReadySignalingDOM binds fine but fails the readiness signal.
132type failingReadySignalingDOM struct {
133	signalingDOM
134	err error
135}
136
137func (d *failingReadySignalingDOM) BindActions(DOMActionHandler) error {
138	return nil
139}
140
141func (d *failingReadySignalingDOM) SignalReady() error {
142	return d.err
143}
144
145func TestReadinessNotGatedOnAsyncHistoryData(t *testing.T) {
146	t.Parallel()
147
148	dom := &signalingActionDOM{signalingDOM: signalingDOM{recordingDOM: recordingDOM{mounts: make(chan string, 4)}}}
149	history := &recordingHistoryService{
150		entries:         []*entity.HistoryEntry{{ID: 1, URL: "https://example.com", Title: "Loaded entry"}},
151		timelineStarted: make(chan struct{}),
152		releaseTimeline: make(chan struct{}),
153	}
154	app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
155
156	require.NoError(t, app.RunWithContext(context.Background()))
157	select {
158	case <-history.timelineStarted:
159	case <-time.After(5 * time.Second):
160		t.Fatal("history timeline was not started asynchronously")
161	}
162	// The async hydration is still blocked, yet readiness already fired:
163	// history-data completion is separate from shell readiness.
164	require.True(t, dom.isReady(), "readiness must not wait for history data")
165	close(history.releaseTimeline)
166}

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 8

Tags 42

↑↓ moveenter openesc close