main ece7682

preview_lifecycle_test.go RawHistoryFolder

1package brokeripc
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/ports"
11	"github.com/bnema/vev/internal/protocol"
12)
13
14// previewTarget builds one valid live preview target on the canonical local
15// authority: it still requires a non-empty route identity for the local daemon.
16func previewTarget() domain.RemoteSessionTarget {
17	return domain.RemoteSessionTarget{
18		Endpoint:      "local",
19		DisplayOrigin: "local",
20		LifecycleID:   domain.SessionLifecycleID{0x11},
21		SessionName:   "work",
22		LiveTabID:     "tab-alpha",
23	}
24}
25
26// previewRequest builds one valid generation-N preview request for a client:
27// the local daemon route plus the exact session/tab target and bounded
28// viewport sent to it. The broker allocates the observation stream itself, so
29// the request names none. The endpoint epoch is assigned at accept, so the
30// test takes it explicitly.
31func previewRequest(t *testing.T, epoch ports.BrokerEpoch, client ports.BrokerService, generation ports.BrokerPreviewGeneration) ports.BrokerPreviewRequest {
32	t.Helper()
33	request := ports.BrokerPreviewRequest{
34		Epoch:      epoch,
35		Connection: client.ConnectionID(),
36		Generation: generation,
37		Route:      ports.BrokerPreviewRoute{Local: true, Policy: openRequest(1).Policy},
38		Preview:    protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: previewTarget(), Width: 80, Height: 24},
39	}
40	require.NoError(t, request.Validate())
41	return request
42}
43
44// serverPreviewGeneration returns the server session's current preview
45// generation: it observes the same replacement bookkeeping startPreview
46// maintains, so a test can prove which generation the server holds current.
47func serverPreviewGeneration(session ports.BrokerCoreService) ports.BrokerPreviewGeneration {
48	server, ok := session.(*serverSession)
49	if !ok {
50		return 0
51	}
52	server.subMu.Lock()
53	defer server.subMu.Unlock()
54	return server.previewGeneration
55}
56
57// serverPreviewOf returns the server session's current preview subscription.
58func serverPreviewOf(session ports.BrokerCoreService) ports.BrokerPreviewSubscription {
59	server, ok := session.(*serverSession)
60	if !ok {
61		return nil
62	}
63	server.subMu.Lock()
64	defer server.subMu.Unlock()
65	return server.preview
66}
67
68// TestPreviewReplacementClosesPrevious proves GO-001/GO-003 on the wire: a
69// second SubscribePreview with a newer generation replaces the first on both
70// ends. The client retires the first subscription, and the server keeps only
71// the newest generation as its current preview.
72func TestPreviewReplacementClosesPrevious(t *testing.T) {
73	e := startEndpoint(t, Config{})
74	client, session := e.pair()
75
76	_, err := client.SubscribePreview(previewRequest(t, e.epoch, client, 1))
77	require.NoError(t, err)
78	require.Eventually(t, func() bool { return serverPreviewOf(session) != nil }, 5*time.Second, 10*time.Millisecond, "the first generation must register server-side")
79
80	second, err := client.SubscribePreview(previewRequest(t, e.epoch, client, 2))
81	require.NoError(t, err)
82	require.NotNil(t, second)
83
84	require.Eventually(t, func() bool { return serverPreviewGeneration(session) == 2 }, 5*time.Second, 10*time.Millisecond, "the second generation must replace the first server-side")
85}
86
87// TestPreviewCancelGenerationFencesStaleCancel proves the CancelPreview
88// generation match: cancelling a superseded generation leaves the current
89// preview registered, while cancelling the current generation clears it.
90func TestPreviewCancelGenerationFencesStaleCancel(t *testing.T) {
91	e := startEndpoint(t, Config{})
92	client, session := e.pair()
93
94	first, err := client.SubscribePreview(previewRequest(t, e.epoch, client, 1))
95	require.NoError(t, err)
96	second, err := client.SubscribePreview(previewRequest(t, e.epoch, client, 2))
97	require.NoError(t, err)
98
99	require.Eventually(t, func() bool { return serverPreviewGeneration(session) == 2 }, 5*time.Second, 10*time.Millisecond, "the second generation must register server-side")
100
101	// A stale cancel for the superseded first generation must not disturb the
102	// current second generation.
103	first.Close()
104	require.Never(t, func() bool { return serverPreviewOf(session) == nil }, 200*time.Millisecond, 10*time.Millisecond, "a stale cancel must not clear the current preview")
105
106	second.Close()
107	require.Eventually(t, func() bool { return serverPreviewOf(session) == nil }, 5*time.Second, 10*time.Millisecond, "cancelling the current generation must clear the preview")
108}
109
110// TestPreviewCloseClearsCurrent proves GO-004: closing the client connection
111// retires its current preview subscription instead of relying on transport
112// EOF for server teardown.
113func TestPreviewCloseClearsCurrent(t *testing.T) {
114	e := startEndpoint(t, Config{})
115	client, session := e.pair()
116
117	sub, err := client.SubscribePreview(previewRequest(t, e.epoch, client, 1))
118	require.NoError(t, err)
119	_ = sub
120	require.Eventually(t, func() bool { return serverPreviewOf(session) != nil }, 5*time.Second, 10*time.Millisecond, "the preview must register server-side")
121
122	require.NoError(t, client.Close())
123	require.Eventually(t, func() bool { return serverPreviewOf(session) == nil }, 5*time.Second, 10*time.Millisecond, "closing the client must retire its preview server-side")
124}
125
126// TestPreviewShutdownClearsServerState proves the server shutdown path stops
127// the replaced forwarder bookkeeping: a session that held a preview releases
128// it when the session shuts down.
129func TestPreviewShutdownClearsServerState(t *testing.T) {
130	e := startEndpoint(t, Config{})
131	client, session := e.pair()
132
133	_, err := client.SubscribePreview(previewRequest(t, e.epoch, client, 1))
134	require.NoError(t, err)
135	require.Eventually(t, func() bool { return serverPreviewOf(session) != nil }, 5*time.Second, 10*time.Millisecond, "the preview must register server-side")
136
137	require.NoError(t, session.Close())
138	require.Eventually(t, func() bool { return serverPreviewOf(session) == nil }, 5*time.Second, 10*time.Millisecond, "session shutdown must release the preview")
139}

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