palette_inventory_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5	"time"
6
7	"github.com/stretchr/testify/require"
8
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/protocol"
11)
12
13// TestPaletteInventoryNamespaceLifecycle pins the overlay interaction
14// namespace: open bumps the generation and resets admit state, close drops
15// relayed rows, and a reopen starts a new namespace.
16func TestPaletteInventoryNamespaceLifecycle(t *testing.T) {
17	rt := &overlayRuntime{}
18	first := openPaletteInventory(rt)
19	require.Equal(t, uint64(1), first)
20	rt.paletteInventoryPublication = 3
21	rt.paletteInventoryGroups = []protocol.NavigationInventorySourceGroup{{SourceKey: "local"}}
22
23	interaction, ok := takePaletteInventoryClose(rt)
24	require.True(t, ok)
25	require.Equal(t, first, interaction)
26
27	closePaletteInventory(rt)
28	if _, ok := takePaletteInventoryClose(rt); ok {
29		t.Fatal("closed palette must not offer a close-demand")
30	}
31	require.Empty(t, rt.paletteInventoryGroups)
32
33	second := openPaletteInventory(rt)
34	require.Equal(t, first+1, second)
35	require.Zero(t, rt.paletteInventoryPublication)
36}
37
38// TestAdmitPaletteInventoryPublication pins admission: newer generations
39// store, older and duplicate generations discard, stale interactions and
40// closed palettes drop, malformed messages never store.
41func TestAdmitPaletteInventoryPublication(t *testing.T) {
42	groups := func() []protocol.NavigationInventorySourceGroup {
43		return []protocol.NavigationInventorySourceGroup{{
44			SourceKey: "local", Status: protocol.NavigationInventorySourceOK,
45			Entries: []protocol.NavigationInventoryEntry{
46				{SourceKey: "local", EntryKey: "aaa/one", Name: "one"},
47			},
48		}}
49	}
50	publication := func(interaction, generation uint64) protocol.NavigationInventoryPublication {
51		return protocol.NavigationInventoryPublication{InteractionGeneration: interaction, PublicationGeneration: generation, Groups: groups()}
52	}
53
54	rt := &overlayRuntime{}
55	interaction := openPaletteInventory(rt)
56
57	require.True(t, admitPaletteInventoryPublication(rt, publication(interaction, 1)))
58	require.Len(t, rt.paletteInventoryGroups, 1)
59	require.False(t, admitPaletteInventoryPublication(rt, publication(interaction, 1)), "duplicate generation must discard")
60	require.False(t, admitPaletteInventoryPublication(rt, publication(interaction, 0)), "older generation must discard")
61	require.True(t, admitPaletteInventoryPublication(rt, publication(interaction, 2)))
62	require.Equal(t, uint64(2), rt.paletteInventoryPublication)
63	require.False(t, admitPaletteInventoryPublication(rt, publication(interaction+7, 3)), "stale interaction must drop")
64
65	closePaletteInventory(rt)
66	require.False(t, admitPaletteInventoryPublication(rt, publication(interaction, 3)), "closed palette must drop")
67
68	other := &overlayRuntime{}
69	openPaletteInventory(other)
70	malformed := publication(other.paletteInventoryInteraction, 1)
71	malformed.Groups = nil
72	malformed.PublicationGeneration = 0
73	require.False(t, admitPaletteInventoryPublication(other, malformed))
74	require.Empty(t, other.paletteInventoryGroups)
75}
76
77// TestPaletteInventoryDemandAndSelectionFlow drives the serving-daemon side
78// end to end through a capturing transport: capability-gated open demand,
79// publication admission with imported-row projection, Enter resolving
80// through opaque keys with bound cause, Selection before close-demand wire
81// order, and overlay teardown that preserves the frozen selection.
82func TestPaletteInventoryDemandAndSelectionFlow(t *testing.T) {
83	p, release := newBlockingPTY(t)
84	defer release()
85	d, current, ac, sends := newManualSessionWithPTYs(t, p)
86	ac.navigationCapabilities = protocol.NavigationCapabilityInventory
87	effect := beginRecentRoutePaletteEffect(t, d, current, ac)
88	// No automation action ID: ordinary keyboard input carries cause zero,
89	// which must still select.
90
91	interaction := d.enterPalette(current, ac)
92	d.sendPaletteInventoryDemand(ac, effect, true, interaction)
93	demandFrame := awaitFrame(t, sends, "NavigationInventoryDemand")
94	demand := decodeServerMessage(t, demandFrame).(protocol.NavigationInventoryDemand)
95	require.True(t, demand.Open)
96	require.Equal(t, interaction, demand.InteractionGeneration)
97
98	published := protocol.NavigationInventoryPublication{
99		InteractionGeneration: interaction, PublicationGeneration: 1,
100		Groups: []protocol.NavigationInventorySourceGroup{{
101			SourceKey: "local", Status: protocol.NavigationInventorySourceOK,
102			Entries: []protocol.NavigationInventoryEntry{
103				{SourceKey: "local", EntryKey: "aaa/zzqimported", Name: "zzqimported", DisplayOrigin: "local", State: "up"},
104			},
105		}},
106	}
107	ac.overlays.paletteMu.Lock()
108	require.True(t, admitPaletteInventoryPublication(ac.overlays, published))
109	ac.overlays.paletteMu.Unlock()
110	d.refreshPalette(ac)
111
112	d.handlePaletteInput(ac, []byte("zzqimported\r"), effect)
113
114	selectionFrame := awaitFrame(t, sends, "NavigationInventorySelection")
115	selection := decodeServerMessage(t, selectionFrame).(protocol.NavigationInventorySelection)
116	require.Equal(t, uint64(0), selection.CauseActionID, "keyboard input carries no automation cause")
117	require.Equal(t, interaction, selection.InteractionGeneration)
118	require.Equal(t, uint64(1), selection.PublicationGeneration)
119	require.Equal(t, "local", selection.SourceKey)
120	require.Equal(t, "aaa/zzqimported", selection.EntryKey)
121
122	closeFrame := awaitFrame(t, sends, "NavigationInventoryDemand")
123	closeDemand, ok := decodeServerMessage(t, closeFrame).(protocol.NavigationInventoryDemand)
124	require.True(t, ok)
125	require.False(t, closeDemand.Open)
126	require.Equal(t, interaction, closeDemand.InteractionGeneration)
127	// Input handling sends controls synchronously; no second close may be queued.
128	for len(sends) > 0 {
129		frame := <-sends
130		require.NotEqual(t, "NavigationInventoryDemand", envelopeMessageName(t, frame.Payload), "selection emits exactly one close")
131	}
132
133	require.False(t, ac.overlays.paletteActive(), "select closes the overlay")
134	ac.overlays.paletteMu.Lock()
135	frozen := ac.overlays.paletteInventorySelected
136	ac.overlays.paletteMu.Unlock()
137	require.NotNil(t, frozen, "the pending selection survives the close so a late failure correlates")
138	require.Equal(t, interaction, frozen.InteractionGeneration)
139	require.Equal(t, "aaa/zzqimported", frozen.EntryKey)
140}
141
142// TestPaletteInventoryRemoteEnterNeverAttaches pins read-only remote
143// vision: Enter on a remote-origin row reports feedback, keeps the palette
144// open, and emits no Selection frame.
145func TestPaletteInventoryRemoteEnterNeverAttaches(t *testing.T) {
146	p, release := newBlockingPTY(t)
147	defer release()
148	d, current, ac, sends := newManualSessionWithPTYs(t, p)
149	ac.navigationCapabilities = protocol.NavigationCapabilityInventory
150	effect := beginRecentRoutePaletteEffect(t, d, current, ac)
151	effect.uiActionID = 123
152
153	interaction := d.enterPalette(current, ac)
154	published := protocol.NavigationInventoryPublication{
155		InteractionGeneration: interaction, PublicationGeneration: 1,
156		Groups: []protocol.NavigationInventorySourceGroup{
157			{
158				SourceKey: "local", Status: protocol.NavigationInventorySourceOK,
159				Entries: []protocol.NavigationInventoryEntry{
160					{SourceKey: "local", EntryKey: "aaa/zzqlocal", Name: "zzqlocal", DisplayOrigin: "local", State: "up"},
161				},
162			},
163			{
164				SourceKey: "arch", Status: protocol.NavigationInventorySourceOK,
165				Entries: []protocol.NavigationInventoryEntry{
166					{SourceKey: "arch", EntryKey: "aaa/zzqremote", Name: "zzqremote", DisplayOrigin: "arch", State: "down", Reason: "unreachable"},
167				},
168			},
169		},
170	}
171	ac.overlays.paletteMu.Lock()
172	require.True(t, admitPaletteInventoryPublication(ac.overlays, published))
173	ac.overlays.paletteMu.Unlock()
174	d.refreshPalette(ac)
175
176	d.handlePaletteInput(ac, []byte("zzqremote\r"), effect)
177
178	ac.overlays.paletteMu.Lock()
179	feedback := ac.overlays.paletteFeedback
180	ac.overlays.paletteMu.Unlock()
181	require.Equal(t, "remote sessions are visible but cannot be attached", feedback)
182	require.True(t, ac.overlays.paletteActive(), "refused remote selection keeps the palette open")
183	select {
184	case frame := <-sends:
185		require.NotEqual(t, "NavigationInventorySelection", envelopeMessageName(t, frame.Payload), "remote Enter must not emit a selection")
186	case <-time.After(200 * time.Millisecond):
187	}
188}
189
190// TestPaletteInventoryFailureCorrelatesToPendingSelection pins durable
191// failure routing: a failure matching the frozen selection records a
192// notice even with the palette closed, shows contextual feedback while
193// the interaction is open, and drops when nothing matches.
194func TestPaletteInventoryFailureCorrelatesToPendingSelection(t *testing.T) {
195	p, release := newBlockingPTY(t)
196	defer release()
197	d, current, ac, _ := newManualSessionWithPTYs(t, p)
198
199	interaction := d.enterPalette(current, ac)
200	selection := protocol.NavigationInventorySelection{
201		InteractionGeneration: interaction, PublicationGeneration: 1,
202		SourceKey: "local", EntryKey: "aaa/zzqimported",
203	}
204	ac.overlays.paletteMu.Lock()
205	freezePaletteInventorySelection(ac.overlays, selection)
206	ac.overlays.paletteMu.Unlock()
207	d.closeExecutedPalette(ac, nil, 1, "")
208
209	matched := protocol.NavigationInventoryFailure{
210		InteractionGeneration: interaction, SourceKey: "local", EntryKey: "aaa/zzqimported",
211		Code: protocol.NavigationInventorySourceGone,
212	}
213	d.handleAttachmentClientMessage(captureAttachmentCapability(current, ac, ac.transport()), matched)
214	require.True(t, hasInventoryNotice(t, ac), "matched failure must record a durable notice")
215
216	stale := protocol.NavigationInventoryFailure{
217		InteractionGeneration: interaction + 1, SourceKey: "local", EntryKey: "aaa/zzqimported",
218		Code: protocol.NavigationInventorySourceGone,
219	}
220	before := countInventoryNotices(t, ac)
221	d.handleAttachmentClientMessage(captureAttachmentCapability(current, ac, ac.transport()), stale)
222	require.Equal(t, before, countInventoryNotices(t, ac), "unmatched failure must drop")
223}
224
225func hasInventoryNotice(t *testing.T, ac *attachedClient) bool {
226	t.Helper()
227	return countInventoryNotices(t, ac) != 0
228}
229
230func countInventoryNotices(t *testing.T, ac *attachedClient) int {
231	t.Helper()
232	toasts, _ := visibleToasts(ac)
233	count := 0
234	for _, toast := range toasts {
235		if toast.Code == domain.NoticeNavigationInventory {
236			count++
237		}
238	}
239	return count
240}
241
242// TestPaletteInventoryEscapeSendsCloseDemand pins the plain-cancel path: a
243// bare Escape cancels the unselected interaction and still stops polling.
244func TestPaletteInventoryEscapeSendsCloseDemand(t *testing.T) {
245	p, release := newBlockingPTY(t)
246	defer release()
247	d, current, ac, sends := newManualSessionWithPTYs(t, p)
248	ac.navigationCapabilities = protocol.NavigationCapabilityInventory
249	effect := beginRecentRoutePaletteEffect(t, d, current, ac)
250
251	interaction := d.enterPalette(current, ac)
252	d.sendPaletteInventoryDemand(ac, effect, true, interaction)
253	awaitFrame(t, sends, "NavigationInventoryDemand")
254
255	d.handlePaletteInput(ac, []byte{0x1b}, effect)
256	closeFrame := awaitFrame(t, sends, "NavigationInventoryDemand")
257	closeDemand, ok := decodeServerMessage(t, closeFrame).(protocol.NavigationInventoryDemand)
258	require.True(t, ok)
259	require.False(t, closeDemand.Open)
260	require.Equal(t, interaction, closeDemand.InteractionGeneration)
261	require.False(t, ac.overlays.paletteActive())
262}
263
264// TestPaletteInventoryDemandRequiresCapability pins the serving-side gate:
265// without the advertised capability bit no demand crosses, even with an
266// admitted effect, and the overlay namespace still opens natively.
267func TestPaletteInventoryDemandRequiresCapability(t *testing.T) {
268	p, release := newBlockingPTY(t)
269	defer release()
270	d, current, ac, sends := newManualSessionWithPTYs(t, p)
271	effect := beginRecentRoutePaletteEffect(t, d, current, ac)
272
273	interaction := d.enterPalette(current, ac)
274	d.sendPaletteInventoryDemand(ac, effect, true, interaction)
275	deadline := time.After(time.Second)
276	for {
277		select {
278		case frame := <-sends:
279			if envelopeMessageName(t, frame.Payload) == "NavigationInventoryDemand" {
280				t.Fatalf("demand without capability must not send")
281			}
282		case <-deadline:
283			require.True(t, ac.overlays.paletteActive())
284			return
285		}
286	}
287}

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