detached ece7682

picker_commit_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/catalogue"
13)
14
15// TestPickerCommitResolvesTheCommittedRow drives the whole picker commit path
16// through the real controller and catalogue: a keypress records the commit
17// decision with its row key, TakeOp hands the key over, and ResolveCommit
18// revalidates it against the latest publication into the exact broker stream
19// request and tab. A publication between commit and resolution must never
20// retarget the commit.
21func TestPickerCommitResolvesTheCommittedRow(t *testing.T) {
22	now := time.Unix(1000, 0)
23	// tabbed publishes session "work" at revision with tabs t1, t2, ... named
24	// in order. A session with tabs is a header; only its tab rows commit.
25	tabbed := func(revision ports.BrokerRevision, names ...string) ports.BrokerSnapshot {
26		tabs := make([]catalogue.RemoteCatalogTab, 0, len(names))
27		for i, name := range names {
28			tabs = append(tabs, pickerTestTab(fmt.Sprintf("t%d", i+1), name, "", false))
29		}
30		return ports.BrokerSnapshot{Epoch: 3, Revision: revision, Daemons: []ports.BrokerDaemonObservation{
31			pickerTestLocalObservation(now, pickerTestTabbed("work", 3, catalogue_Up, 1, "t1", tabs...)),
32		}}
33	}
34	tests := []struct {
35		name string
36		// row is the label of the tab row the cursor commits.
37		row         string
38		republished *ports.BrokerSnapshot
39		wantTab     domain.TabStableID
40		wantErr     pickerCatalogueErrorCode
41	}{
42		{name: "first tab commits its tab", row: "shell", wantTab: "t1"},
43		{name: "second tab commits its tab", row: "build", wantTab: "t2"},
44		{name: "unrelated republication keeps the commit", row: "build", republished: ptr(tabbed(2, "shell", "build", "logs")), wantTab: "t2"},
45		{name: "tab closed after commit is refused", row: "build", republished: ptr(tabbed(2, "shell")), wantErr: pickerCatalogueGone},
46	}
47	for _, tt := range tests {
48		t.Run(tt.name, func(t *testing.T) {
49			controller, _ := pickerTestController(t)
50			controller.ApplySnapshot(tabbed(1, "shell", "build"))
51			key := pickerTabKey(t, controller.Catalogue().RecentLines(), "work", tt.row)
52			for i := 0; i < 16; i++ {
53				if current, ok := controller.CursorKey(); ok && current == key {
54					break
55				}
56				require.True(t, controller.ConsumeTerminalRead([]byte("j")))
57			}
58			require.True(t, controller.ConsumeTerminalRead([]byte("\r")))
59			op, committed := controller.TakeOp()
60			require.True(t, op.commit)
61			require.Equal(t, key, committed, "the commit captures the row under the cursor")
62
63			if tt.republished != nil {
64				controller.ApplySnapshot(*tt.republished)
65			}
66			request, tab, err := controller.ResolveCommit(committed, pickerTestBase())
67
68			if tt.wantErr != 0 {
69				require.True(t, pickerCatalogueErrorIs(err, tt.wantErr), "want %v, got %v", tt.wantErr, err)
70				return
71			}
72			require.NoError(t, err)
73			require.Equal(t, ports.BrokerAdmissionExact, request.Admission)
74			require.Equal(t, "work", request.Target.SessionName)
75			require.Equal(t, pickerTestBase().Stream, request.Stream)
76			require.Equal(t, tt.wantTab, tab.preferred)
77		})
78	}
79}
80
81// TestPickerCatalogueResolveTargetKinds covers every selection kind a commit
82// key can carry: destinations resolve to their admission, while a CLI-only
83// attach-by-name identity is refused as a picker commit.
84func TestPickerCatalogueResolveTargetKinds(t *testing.T) {
85	observation := pickerTestLocalObservation(time.Unix(1000, 0), pickerTestSession("alpha", 1, catalogue_Up))
86	tests := []struct {
87		name          string
88		ref           pickerSelectionRef
89		wantAdmission ports.BrokerStreamAdmission
90		wantErr       pickerCatalogueErrorCode
91	}{
92		{name: "exact session", ref: pickerSelectionRef{kind: pickerSelectionExact, lifecycle: pickerTestLifecycle(1), name: "alpha"}, wantAdmission: ports.BrokerAdmissionExact},
93		{name: "create named", ref: pickerSelectionRef{kind: pickerSelectionCreateNamed, createName: "fresh"}, wantAdmission: ports.BrokerAdmissionCreateNamed},
94		{name: "create named with invalid name", ref: pickerSelectionRef{kind: pickerSelectionCreateNamed, createName: "bad name/"}, wantErr: pickerCatalogueInvalidName},
95		{name: "create ephemeral", ref: pickerSelectionRef{kind: pickerSelectionCreateEphemeral}, wantAdmission: ports.BrokerAdmissionCreateEphemeral},
96		{name: "attach named is not a picker destination", ref: pickerSelectionRef{kind: pickerSelectionAttachNamed, name: "alpha"}, wantErr: pickerCatalogueUnavailable},
97		{name: "unknown key", wantErr: pickerCatalogueUnknown},
98	}
99	for _, tt := range tests {
100		t.Run(tt.name, func(t *testing.T) {
101			catalogue, _ := pickerTestCatalogue(t)
102			require.True(t, catalogue.Apply(ports.BrokerSnapshot{Epoch: 3, Revision: 1, Daemons: []ports.BrokerDaemonObservation{observation}}))
103			key := "missing"
104			if tt.ref.kind != 0 {
105				key = "injected"
106				tt.ref.epoch = 3
107				tt.ref.local = true
108				catalogue.mu.Lock()
109				catalogue.refs[key] = tt.ref
110				catalogue.mu.Unlock()
111			}
112
113			request, _, err := catalogue.ResolveTarget(key, pickerTestBase())
114
115			if tt.wantErr != 0 {
116				require.True(t, pickerCatalogueErrorIs(err, tt.wantErr), "want %v, got %v", tt.wantErr, err)
117				return
118			}
119			require.NoError(t, err)
120			require.Equal(t, tt.wantAdmission, request.Admission)
121		})
122	}
123}
124
125func ptr[T any](v T) *T { return &v }

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