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}