route_ledger_test.go RawHistoryFolder

1package client
2
3import (
4	"fmt"
5	"testing"
6	"time"
7
8	"github.com/stretchr/testify/require"
9
10	"github.com/bnema/vev/internal/domain"
11	"github.com/bnema/vev/internal/ports"
12	"github.com/bnema/vev/internal/protocol"
13	"github.com/bnema/vev/internal/protocol/catalogue"
14)
15
16// Route ledger (Plan 003 C4/E3), ported from main's routes_test observation
17// and ordering cases onto the broker catalogue.
18
19const routeTestRemote = "vev@box"
20
21func routeTestSnapshot(daemons ...ports.BrokerDaemonObservation) ports.BrokerSnapshot {
22	return ports.BrokerSnapshot{Epoch: 3, Revision: 1, Daemons: daemons}
23}
24
25func routeTestLocal(sessions ...catalogue.RemoteCatalogSession) ports.BrokerDaemonObservation {
26	return pickerTestLocalObservation(time.Unix(1000, 0), sessions...)
27}
28
29func routeTestRemoteHost(sessions ...catalogue.RemoteCatalogSession) ports.BrokerDaemonObservation {
30	return pickerTestRemoteObservation(routeTestRemote, 1, 2, time.Unix(1000, 0), sessions...)
31}
32
33func routeTestLive(name string, seed byte, lastUsed uint64, attention ...bool) catalogue.RemoteCatalogSession {
34	tabs := make([]catalogue.RemoteCatalogTab, 0, len(attention))
35	for i, bell := range attention {
36		tabs = append(tabs, pickerTestTab(fmt.Sprintf("t%d", i), fmt.Sprintf("tab%d", i), "", bell))
37	}
38	return pickerTestTabbed(name, seed, catalogue.RemoteCatalogSessionUp, lastUsed, "", tabs...)
39}
40
41func routeTestStopped(name string, seed byte, lastUsed uint64) catalogue.RemoteCatalogSession {
42	return pickerTestTabbed(name, seed, catalogue.RemoteCatalogSessionDown, lastUsed, "")
43}
44
45func routeTestActive(local bool, name string, seed byte) routeActive {
46	authority := routeAuthority{local: true}
47	if !local {
48		authority = routeAuthority{endpoint: routeTestRemote}
49	}
50	return routeActive{known: true, authority: authority, target: protocol.ExactSessionTarget{LifecycleID: pickerTestLifecycle(seed), SessionName: name}}
51}
52
53func routeEntryNames(snapshot protocol.RecentRouteSnapshot) []string {
54	names := make([]string, 0, len(snapshot.Entries))
55	for _, entry := range snapshot.Entries {
56		names = append(names, entry.Name)
57	}
58	return names
59}
60
61func TestRouteLedgerOrdering(t *testing.T) {
62	tests := []struct {
63		name   string
64		visits []routeActive
65		build  ports.BrokerSnapshot
66		active routeActive
67		want   []string
68	}{
69		{
70			name: "live local by recency, then remote in catalogue order, then stopped",
71			build: routeTestSnapshot(
72				routeTestLocal(routeTestLive("old", 1, 1), routeTestStopped("gone", 2, 9), routeTestLive("new", 3, 5), routeTestLive("here", 4, 7)),
73				routeTestRemoteHost(routeTestLive("r2", 5, 0), routeTestLive("r1", 6, 0), routeTestStopped("rs", 7, 0)),
74			),
75			active: routeTestActive(true, "here", 4),
76			want:   []string{"new", "old", "r2", "r1", "gone", "rs"},
77		},
78		{
79			name:   "this client's attach recency comes first",
80			visits: []routeActive{routeTestActive(false, "r1", 6), routeTestActive(true, "old", 1)},
81			build: routeTestSnapshot(
82				routeTestLocal(routeTestLive("old", 1, 1), routeTestLive("new", 3, 5), routeTestLive("here", 4, 7)),
83				routeTestRemoteHost(routeTestLive("r2", 5, 0), routeTestLive("r1", 6, 0)),
84			),
85			active: routeTestActive(true, "here", 4),
86			want:   []string{"old", "r1", "new", "r2"},
87		},
88		{
89			name:   "broken sessions are never routes",
90			build:  routeTestSnapshot(routeTestLocal(routeTestLive("ok", 1, 1), pickerTestTabbed("bad", 2, catalogue.RemoteCatalogSessionBroken, 5, ""))),
91			active: routeTestActive(true, "here", 4),
92			want:   []string{"ok"},
93		},
94	}
95	for _, tt := range tests {
96		t.Run(tt.name, func(t *testing.T) {
97			ledger := newRouteLedger()
98			for _, visit := range tt.visits {
99				ledger.build(tt.build, visit)
100			}
101			snapshot, _ := ledger.build(tt.build, tt.active)
102			require.NoError(t, snapshot.Validate())
103			require.Equal(t, tt.want, routeEntryNames(snapshot))
104			require.Equal(t, tt.active.target, snapshot.ActiveEntry.Target, "the attached lifecycle is metadata only")
105		})
106	}
107}
108
109func TestRouteLedgerActivePreviousAndHome(t *testing.T) {
110	build := routeTestSnapshot(
111		routeTestLocal(routeTestLive("a", 1, 2), routeTestLive("b", 2, 1)),
112		routeTestRemoteHost(routeTestLive("r", 3, 0)),
113	)
114	tests := []struct {
115		name         string
116		active       routeActive
117		wantKind     protocol.RouteKind
118		wantLabel    string
119		wantHome     bool
120		wantPrevious string
121		wantHosts    []protocol.RouteKind
122	}{
123		{name: "local attachment is home", active: routeTestActive(true, "a", 1), wantKind: protocol.RouteKindLocal, wantHome: true, wantPrevious: "r", wantHosts: []protocol.RouteKind{protocol.RouteKindRemote}},
124		{name: "remote attachment offers the local host", active: routeTestActive(false, "r", 3), wantKind: protocol.RouteKindRemote, wantLabel: routeTestRemote, wantPrevious: "a", wantHosts: []protocol.RouteKind{protocol.RouteKindLocal}},
125		{name: "an unobserved attachment is synthesized", active: routeTestActive(true, "fresh", 9), wantKind: protocol.RouteKindLocal, wantHome: true, wantPrevious: "r", wantHosts: []protocol.RouteKind{protocol.RouteKindRemote}},
126	}
127	for _, tt := range tests {
128		t.Run(tt.name, func(t *testing.T) {
129			ledger := newRouteLedger()
130			// Visit the other kind first so Previous names it.
131			if tt.active.authority.local {
132				ledger.build(build, routeTestActive(false, "r", 3))
133			} else {
134				ledger.build(build, routeTestActive(true, "a", 1))
135			}
136			snapshot, changed := ledger.build(build, tt.active)
137			require.True(t, changed)
138			require.NoError(t, snapshot.Validate())
139			require.Equal(t, tt.wantKind, snapshot.ActiveEntry.Kind)
140			require.Equal(t, tt.wantLabel, snapshot.ActiveEntry.HostLabel)
141			require.Equal(t, snapshot.Active, protocol.RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation})
142			if tt.wantHome {
143				require.Equal(t, snapshot.Active, snapshot.Home)
144			} else {
145				require.True(t, snapshot.Home.IsZero())
146			}
147			previous, ok := ledger.resolve(snapshot.Previous)
148			require.True(t, ok)
149			require.Equal(t, tt.wantPrevious, previous.target.SessionName)
150			require.True(t, snapshot.ActiveEntry.Visited)
151			var visited []string
152			for _, entry := range snapshot.Entries {
153				if entry.Visited {
154					visited = append(visited, entry.Name)
155				}
156			}
157			require.Equal(t, []string{tt.wantPrevious}, visited, "only attached routes are visited")
158			kinds := make([]protocol.RouteKind, 0, len(snapshot.Hosts))
159			for _, host := range snapshot.Hosts {
160				kinds = append(kinds, host.Kind)
161				resolved, ok := ledger.resolve(protocol.RouteRef{Key: host.Key, Generation: host.Generation})
162				require.True(t, ok)
163				require.True(t, resolved.host)
164				require.NotEqual(t, tt.active.authority, resolved.authority, "the serving daemon is never a host row")
165			}
166			require.Equal(t, tt.wantHosts, kinds)
167		})
168	}
169}
170
171func TestRouteLedgerStableReferencesAndGeneration(t *testing.T) {
172	ledger := newRouteLedger()
173	active := routeTestActive(true, "here", 4)
174	first, changed := ledger.build(routeTestSnapshot(routeTestLocal(routeTestLive("a", 1, 1), routeTestLive("here", 4, 2))), active)
175	require.True(t, changed)
176	require.Equal(t, uint64(1), first.Generation)
177
178	same, changed := ledger.build(routeTestSnapshot(routeTestLocal(routeTestLive("a", 1, 1), routeTestLive("here", 4, 2))), active)
179	require.False(t, changed, "an unchanged publication never advances the generation")
180	require.Equal(t, first, same)
181
182	grown, changed := ledger.build(routeTestSnapshot(routeTestLocal(routeTestLive("a", 1, 1), routeTestLive("b", 2, 3), routeTestLive("here", 4, 2))), active)
183	require.True(t, changed)
184	require.Equal(t, uint64(2), grown.Generation)
185	require.Equal(t, first.Active, grown.Active, "a lifecycle keeps its reference")
186	require.Equal(t, first.Entries[0].Key, grown.Entries[1].Key)
187
188	replaced, _ := ledger.build(routeTestSnapshot(routeTestLocal(routeTestLive("a", 5, 1), routeTestLive("here", 4, 2))), active)
189	require.NotEqual(t, first.Entries[0].Key, replaced.Entries[0].Key, "a new lifecycle of the same name is a new reference")
190	_, ok := ledger.resolve(protocol.RouteRef{Key: first.Entries[0].Key, Generation: 1})
191	require.False(t, ok, "a retired lifecycle no longer resolves")
192}
193
194// TestRouteLedgerAttention ports main's local and remote attention
195// observations: a session carries attention while any of its tabs does, and
196// onsets are ordered as the client observed them across daemons.
197func TestRouteLedgerAttention(t *testing.T) {
198	active := routeTestActive(true, "here", 4)
199	steps := []struct {
200		name  string
201		build ports.BrokerSnapshot
202		want  map[string]bool
203		order []string
204	}{
205		{
206			name:  "quiet",
207			build: routeTestSnapshot(routeTestLocal(routeTestLive("here", 4, 9), routeTestLive("loc", 1, 1, false, false)), routeTestRemoteHost(routeTestLive("rem", 2, 0, false))),
208			want:  map[string]bool{"loc": false, "rem": false},
209		},
210		{
211			name:  "remote bell first",
212			build: routeTestSnapshot(routeTestLocal(routeTestLive("here", 4, 9), routeTestLive("loc", 1, 1, false, false)), routeTestRemoteHost(routeTestLive("rem", 2, 0, true))),
213			want:  map[string]bool{"loc": false, "rem": true},
214			order: []string{"rem"},
215		},
216		{
217			name:  "then a local tab bell",
218			build: routeTestSnapshot(routeTestLocal(routeTestLive("here", 4, 9), routeTestLive("loc", 1, 1, false, true)), routeTestRemoteHost(routeTestLive("rem", 2, 0, true))),
219			want:  map[string]bool{"loc": true, "rem": true},
220			order: []string{"rem", "loc"},
221		},
222		{
223			name:  "remote cleared, then rings again later",
224			build: routeTestSnapshot(routeTestLocal(routeTestLive("here", 4, 9), routeTestLive("loc", 1, 1, false, true)), routeTestRemoteHost(routeTestLive("rem", 2, 0, false))),
225			want:  map[string]bool{"loc": true, "rem": false},
226			order: []string{"loc"},
227		},
228		{
229			name:  "rings again",
230			build: routeTestSnapshot(routeTestLocal(routeTestLive("here", 4, 9), routeTestLive("loc", 1, 1, false, true)), routeTestRemoteHost(routeTestLive("rem", 2, 0, true))),
231			want:  map[string]bool{"loc": true, "rem": true},
232			order: []string{"loc", "rem"},
233		},
234	}
235	ledger := newRouteLedger()
236	for _, step := range steps {
237		t.Run(step.name, func(t *testing.T) {
238			snapshot, _ := ledger.build(step.build, active)
239			require.NoError(t, snapshot.Validate())
240			got := map[string]bool{}
241			seq := map[string]uint64{}
242			for _, entry := range snapshot.Entries {
243				got[entry.Name] = entry.Attention
244				if entry.Attention {
245					require.NotZero(t, entry.AttentionSeq)
246					seq[entry.Name] = entry.AttentionSeq
247				} else {
248					require.Zero(t, entry.AttentionSeq)
249				}
250			}
251			require.Equal(t, step.want, got)
252			for i := 1; i < len(step.order); i++ {
253				require.Less(t, seq[step.order[i-1]], seq[step.order[i]], "%s rang before %s", step.order[i-1], step.order[i])
254			}
255		})
256	}
257}
258
259// TestRouteLedgerVisitedPrefixPrecedesRingingRoutes pins the order the
260// daemon's status history relies on: visited routes stay a prefix of Entries
261// even when an unvisited route on a higher tier rings, so ringing routes never
262// shift the jump-recent ranks of visited ones.
263func TestRouteLedgerVisitedPrefixPrecedesRingingRoutes(t *testing.T) {
264	tests := []struct {
265		name    string
266		visited routeActive
267		build   ports.BrokerSnapshot
268	}{
269		{
270			name:    "visited stopped local before ringing live local",
271			visited: routeTestActive(true, "old", 2),
272			build:   routeTestSnapshot(routeTestLocal(routeTestLive("here", 4, 9), routeTestLive("loud", 1, 8, true), routeTestStopped("old", 2, 1))),
273		},
274		{
275			name:    "visited remote before ringing live local",
276			visited: routeTestActive(false, "r", 3),
277			build:   routeTestSnapshot(routeTestLocal(routeTestLive("here", 4, 9), routeTestLive("loud", 1, 8, true)), routeTestRemoteHost(routeTestLive("r", 3, 0))),
278		},
279	}
280	for _, tt := range tests {
281		t.Run(tt.name, func(t *testing.T) {
282			ledger := newRouteLedger()
283			ledger.build(tt.build, tt.visited)
284			snapshot, _ := ledger.build(tt.build, routeTestActive(true, "here", 4))
285			require.NoError(t, snapshot.Validate())
286			require.Len(t, snapshot.Entries, 2)
287			require.True(t, snapshot.Entries[0].Visited, "the visited route leads")
288			require.Equal(t, tt.visited.target.SessionName, snapshot.Entries[0].Name)
289			require.False(t, snapshot.Entries[1].Visited)
290			require.True(t, snapshot.Entries[1].Attention, "the ringing route follows")
291			require.Equal(t, "loud", snapshot.Entries[1].Name)
292		})
293	}
294}
295
296func TestRouteLedgerBounds(t *testing.T) {
297	sessions := make([]catalogue.RemoteCatalogSession, 0, protocol.RouteSnapshotMaxEntries+5)
298	for i := 0; i < protocol.RouteSnapshotMaxEntries+5; i++ {
299		sessions = append(sessions, routeTestLive(fmt.Sprintf("s%02d", i), byte(i+10), uint64(i)))
300	}
301	incompatible := pickerTestRemoteObservation("old@box", 3, 4, time.Unix(1000, 0))
302	incompatible.Availability = domain.RemoteAvailabilityIncompatible
303	ledger := newRouteLedger()
304	snapshot, changed := ledger.build(routeTestSnapshot(routeTestLocal(sessions...), incompatible), routeTestActive(true, "s00", 10))
305	require.True(t, changed)
306	require.NoError(t, snapshot.Validate())
307	require.Len(t, snapshot.Entries, protocol.RouteSnapshotMaxEntries)
308	require.Empty(t, snapshot.Hosts, "an incompatible daemon is never a creation host")
309}

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