detached 01ca71d

feat(broker): freeze broker contracts and boundaries

commit
01ca71dfc1eff8552064efd175b6eb892a1dfe64
author
brice · · 2026-09-16 07:13 UTC
parent
25169d5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +2953 -1
M boundary_test.go +61 -0
@@ -100,6 +100,36 @@ func TestImportBoundaryNegativeFixtures(t *testing.T) {
100100 {"other adapter rejects usecase", modulePath + "/internal/adapters/ipc", modulePath + "/internal/usecase/client", false, false},
101101 {"usecase test may use wire fixture", modulePath + "/internal/usecase/client", modulePath + "/internal/protocol/wire", true, true},
102102 {"pkg test still rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", true, false},
103+ // P1.1 broker ownership: broker consumes ports/protocol/domain
104+ // (catalogue counts as protocol family) and no sibling use case.
105+ {"broker accepts ports", modulePath + "/internal/usecase/broker", modulePath + "/internal/ports", false, true},
106+ {"broker accepts protocol", modulePath + "/internal/usecase/broker", modulePath + "/internal/protocol", false, true},
107+ {"broker accepts domain", modulePath + "/internal/usecase/broker", modulePath + "/internal/domain", false, true},
108+ {"broker accepts catalogue", modulePath + "/internal/usecase/broker", modulePath + "/internal/protocol/catalogue", false, true},
109+ {"broker accepts own subpackage", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/broker/streams", false, true},
110+ {"broker subpackage accepts broker root", modulePath + "/internal/usecase/broker/streams", modulePath + "/internal/usecase/broker", false, true},
111+ {"broker rejects client", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/client", false, false},
112+ {"broker rejects daemon", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/daemon", false, false},
113+ {"broker rejects remotes", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/remotes", false, false},
114+ {"broker rejects sibling usecase", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/picker", false, false},
115+ {"broker rejects adapter", modulePath + "/internal/usecase/broker", modulePath + "/internal/adapters/ipc", false, false},
116+ {"broker rejects broker adapter", modulePath + "/internal/usecase/broker", modulePath + "/internal/adapters/remote", false, false},
117+ {"broker rejects wire", modulePath + "/internal/usecase/broker", modulePath + "/internal/protocol/wire", false, false},
118+ {"broker rejects app", modulePath + "/internal/usecase/broker", modulePath + "/internal/app", false, false},
119+ {"broker rejects persist", modulePath + "/internal/usecase/broker", modulePath + "/internal/persist", false, false},
120+ // P1.1 client/daemon must not own broker, each other, or remote discovery.
121+ {"client rejects daemon", modulePath + "/internal/usecase/client", modulePath + "/internal/usecase/daemon", false, false},
122+ {"client rejects broker", modulePath + "/internal/usecase/client", modulePath + "/internal/usecase/broker", false, false},
123+ {"client rejects remotes", modulePath + "/internal/usecase/client", modulePath + "/internal/usecase/remotes", false, false},
124+ {"client rejects broker adapter", modulePath + "/internal/usecase/client", modulePath + "/internal/adapters/remote", false, false},
125+ {"daemon rejects client", modulePath + "/internal/usecase/daemon", modulePath + "/internal/usecase/client", false, false},
126+ {"daemon rejects broker", modulePath + "/internal/usecase/daemon", modulePath + "/internal/usecase/broker", false, false},
127+ {"daemon rejects remotes registry", modulePath + "/internal/usecase/daemon", modulePath + "/internal/usecase/remotes", false, false},
128+ {"daemon rejects remote adapter", modulePath + "/internal/usecase/daemon", modulePath + "/internal/adapters/remote", false, false},
129+ {"daemon rejects app", modulePath + "/internal/usecase/daemon", modulePath + "/internal/app", false, false},
130+ {"remotes rejects client", modulePath + "/internal/usecase/remotes", modulePath + "/internal/usecase/client", false, false},
131+ {"remotes rejects daemon", modulePath + "/internal/usecase/remotes", modulePath + "/internal/usecase/daemon", false, false},
132+ {"remotes rejects broker", modulePath + "/internal/usecase/remotes", modulePath + "/internal/usecase/broker", false, false},
103133 }
104134 for _, tt := range tests {
105135 t.Run(tt.name, func(t *testing.T) {
@@ -185,9 +215,40 @@ func dependencyAllowed(source, target string, testFile bool) (bool, error) {
185215 if source == modulePath+"/internal/adapters/snapshot" && target == modulePath+"/internal/usecase/snapshot" {
186216 return true, nil
187217 }
218+ if packageImportDenied(source, target) {
219+ return false, nil
220+ }
188221 return productionDependencies[sourceLayer][targetLayer], nil
189222 }
190223
224+// packageImportDenied encodes ADR 001 ownership at package granularity for
225+// production files. Layer rules alone permit any usecase-to-usecase import;
226+// these denies keep broker, client, daemon, and remotes from owning each
227+// other, and keep the broker free of every sibling use case. Test files stay
228+// exempt so composition tests can wire adapters and orchestrators together.
229+func packageImportDenied(source, target string) bool {
230+ brokerPkg := modulePath + "/internal/usecase/broker"
231+ clientPkg := modulePath + "/internal/usecase/client"
232+ daemonPkg := modulePath + "/internal/usecase/daemon"
233+ remotesPkg := modulePath + "/internal/usecase/remotes"
234+ usecasePrefix := modulePath + "/internal/usecase/"
235+ under := func(path, root string) bool {
236+ return path == root || strings.HasPrefix(path, root+"/")
237+ }
238+ switch {
239+ case under(source, brokerPkg):
240+ return !under(target, brokerPkg) && strings.HasPrefix(target, usecasePrefix)
241+ case under(source, clientPkg):
242+ return under(target, daemonPkg) || under(target, brokerPkg) || under(target, remotesPkg)
243+ case under(source, daemonPkg):
244+ return under(target, clientPkg) || under(target, brokerPkg) || under(target, remotesPkg)
245+ case under(source, remotesPkg):
246+ return under(target, clientPkg) || under(target, daemonPkg) || under(target, brokerPkg)
247+ default:
248+ return false
249+ }
250+}
251+
191252 func testSupportDependencyAllowed(source string, target packageLayer) bool {
192253 switch {
193254 case source == modulePath+"/internal/ports/mocks":
@@ -100,6 +100,36 @@ func TestImportBoundaryNegativeFixtures(t *testing.T) {
100 {"other adapter rejects usecase", modulePath + "/internal/adapters/ipc", modulePath + "/internal/usecase/client", false, false},100 {"other adapter rejects usecase", modulePath + "/internal/adapters/ipc", modulePath + "/internal/usecase/client", false, false},
101 {"usecase test may use wire fixture", modulePath + "/internal/usecase/client", modulePath + "/internal/protocol/wire", true, true},101 {"usecase test may use wire fixture", modulePath + "/internal/usecase/client", modulePath + "/internal/protocol/wire", true, true},
102 {"pkg test still rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", true, false},102 {"pkg test still rejects internal", modulePath + "/pkg/rawterm", modulePath + "/internal/domain", true, false},
103+ // P1.1 broker ownership: broker consumes ports/protocol/domain
104+ // (catalogue counts as protocol family) and no sibling use case.
105+ {"broker accepts ports", modulePath + "/internal/usecase/broker", modulePath + "/internal/ports", false, true},
106+ {"broker accepts protocol", modulePath + "/internal/usecase/broker", modulePath + "/internal/protocol", false, true},
107+ {"broker accepts domain", modulePath + "/internal/usecase/broker", modulePath + "/internal/domain", false, true},
108+ {"broker accepts catalogue", modulePath + "/internal/usecase/broker", modulePath + "/internal/protocol/catalogue", false, true},
109+ {"broker accepts own subpackage", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/broker/streams", false, true},
110+ {"broker subpackage accepts broker root", modulePath + "/internal/usecase/broker/streams", modulePath + "/internal/usecase/broker", false, true},
111+ {"broker rejects client", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/client", false, false},
112+ {"broker rejects daemon", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/daemon", false, false},
113+ {"broker rejects remotes", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/remotes", false, false},
114+ {"broker rejects sibling usecase", modulePath + "/internal/usecase/broker", modulePath + "/internal/usecase/picker", false, false},
115+ {"broker rejects adapter", modulePath + "/internal/usecase/broker", modulePath + "/internal/adapters/ipc", false, false},
116+ {"broker rejects broker adapter", modulePath + "/internal/usecase/broker", modulePath + "/internal/adapters/remote", false, false},
117+ {"broker rejects wire", modulePath + "/internal/usecase/broker", modulePath + "/internal/protocol/wire", false, false},
118+ {"broker rejects app", modulePath + "/internal/usecase/broker", modulePath + "/internal/app", false, false},
119+ {"broker rejects persist", modulePath + "/internal/usecase/broker", modulePath + "/internal/persist", false, false},
120+ // P1.1 client/daemon must not own broker, each other, or remote discovery.
121+ {"client rejects daemon", modulePath + "/internal/usecase/client", modulePath + "/internal/usecase/daemon", false, false},
122+ {"client rejects broker", modulePath + "/internal/usecase/client", modulePath + "/internal/usecase/broker", false, false},
123+ {"client rejects remotes", modulePath + "/internal/usecase/client", modulePath + "/internal/usecase/remotes", false, false},
124+ {"client rejects broker adapter", modulePath + "/internal/usecase/client", modulePath + "/internal/adapters/remote", false, false},
125+ {"daemon rejects client", modulePath + "/internal/usecase/daemon", modulePath + "/internal/usecase/client", false, false},
126+ {"daemon rejects broker", modulePath + "/internal/usecase/daemon", modulePath + "/internal/usecase/broker", false, false},
127+ {"daemon rejects remotes registry", modulePath + "/internal/usecase/daemon", modulePath + "/internal/usecase/remotes", false, false},
128+ {"daemon rejects remote adapter", modulePath + "/internal/usecase/daemon", modulePath + "/internal/adapters/remote", false, false},
129+ {"daemon rejects app", modulePath + "/internal/usecase/daemon", modulePath + "/internal/app", false, false},
130+ {"remotes rejects client", modulePath + "/internal/usecase/remotes", modulePath + "/internal/usecase/client", false, false},
131+ {"remotes rejects daemon", modulePath + "/internal/usecase/remotes", modulePath + "/internal/usecase/daemon", false, false},
132+ {"remotes rejects broker", modulePath + "/internal/usecase/remotes", modulePath + "/internal/usecase/broker", false, false},
103 }133 }
104 for _, tt := range tests {134 for _, tt := range tests {
105 t.Run(tt.name, func(t *testing.T) {135 t.Run(tt.name, func(t *testing.T) {
@@ -185,9 +215,40 @@ func dependencyAllowed(source, target string, testFile bool) (bool, error) {
185 if source == modulePath+"/internal/adapters/snapshot" && target == modulePath+"/internal/usecase/snapshot" {215 if source == modulePath+"/internal/adapters/snapshot" && target == modulePath+"/internal/usecase/snapshot" {
186 return true, nil216 return true, nil
187 }217 }
218+ if packageImportDenied(source, target) {
219+ return false, nil
220+ }
188 return productionDependencies[sourceLayer][targetLayer], nil221 return productionDependencies[sourceLayer][targetLayer], nil
189 }222 }
190 223
224+// packageImportDenied encodes ADR 001 ownership at package granularity for
225+// production files. Layer rules alone permit any usecase-to-usecase import;
226+// these denies keep broker, client, daemon, and remotes from owning each
227+// other, and keep the broker free of every sibling use case. Test files stay
228+// exempt so composition tests can wire adapters and orchestrators together.
229+func packageImportDenied(source, target string) bool {
230+ brokerPkg := modulePath + "/internal/usecase/broker"
231+ clientPkg := modulePath + "/internal/usecase/client"
232+ daemonPkg := modulePath + "/internal/usecase/daemon"
233+ remotesPkg := modulePath + "/internal/usecase/remotes"
234+ usecasePrefix := modulePath + "/internal/usecase/"
235+ under := func(path, root string) bool {
236+ return path == root || strings.HasPrefix(path, root+"/")
237+ }
238+ switch {
239+ case under(source, brokerPkg):
240+ return !under(target, brokerPkg) && strings.HasPrefix(target, usecasePrefix)
241+ case under(source, clientPkg):
242+ return under(target, daemonPkg) || under(target, brokerPkg) || under(target, remotesPkg)
243+ case under(source, daemonPkg):
244+ return under(target, clientPkg) || under(target, brokerPkg) || under(target, remotesPkg)
245+ case under(source, remotesPkg):
246+ return under(target, clientPkg) || under(target, daemonPkg) || under(target, brokerPkg)
247+ default:
248+ return false
249+ }
250+}
251+
191 func testSupportDependencyAllowed(source string, target packageLayer) bool {252 func testSupportDependencyAllowed(source string, target packageLayer) bool {
192 switch {253 switch {
193 case source == modulePath+"/internal/ports/mocks":254 case source == modulePath+"/internal/ports/mocks":
M docs/architecture.md +1 -1
@@ -14,7 +14,7 @@ vev uses a hexagonal core with typed session messages at the client and daemon b
1414 carrying `Envelope{Payload}` (one complete serialized directional
1515 envelope, no type byte).
1616 - `internal/ports`: application-facing interfaces and the values required by those interfaces. It contains no codecs, raw frames, environment policy, or worker implementations.
17-- `internal/usecase`: client, daemon, and supporting application behavior. Production use cases may consume semantic protocol packages but never `protocol/wire` or concrete adapters.
17+- `internal/usecase`: client, daemon, broker, and supporting application behavior. Production use cases may consume semantic protocol packages but never `protocol/wire` or concrete adapters. The broker may use its own subpackages but cannot import sibling use cases. Client, daemon, and remote-registry use cases cannot own or import each other; they collaborate only through ports and application composition.
1818 - `internal/adapters/sessionwire`: translates between typed session
1919 connections and raw wire transports: directional `oneof` envelope
2020 wrapping/unwrapping, the staged preamble state machine
@@ -14,7 +14,7 @@ vev uses a hexagonal core with typed session messages at the client and daemon b
14 carrying `Envelope{Payload}` (one complete serialized directional14 carrying `Envelope{Payload}` (one complete serialized directional
15 envelope, no type byte).15 envelope, no type byte).
16 - `internal/ports`: application-facing interfaces and the values required by those interfaces. It contains no codecs, raw frames, environment policy, or worker implementations.16 - `internal/ports`: application-facing interfaces and the values required by those interfaces. It contains no codecs, raw frames, environment policy, or worker implementations.
17-- `internal/usecase`: client, daemon, and supporting application behavior. Production use cases may consume semantic protocol packages but never `protocol/wire` or concrete adapters.17+- `internal/usecase`: client, daemon, broker, and supporting application behavior. Production use cases may consume semantic protocol packages but never `protocol/wire` or concrete adapters. The broker may use its own subpackages but cannot import sibling use cases. Client, daemon, and remote-registry use cases cannot own or import each other; they collaborate only through ports and application composition.
18 - `internal/adapters/sessionwire`: translates between typed session18 - `internal/adapters/sessionwire`: translates between typed session
19 connections and raw wire transports: directional `oneof` envelope19 connections and raw wire transports: directional `oneof` envelope
20 wrapping/unwrapping, the staged preamble state machine20 wrapping/unwrapping, the staged preamble state machine
A internal/app/connectivity_matrix.go +229 -0
@@ -0,0 +1,229 @@
1+package app
2+
3+// Connectivity migration matrix (Plan 001 P1.2).
4+//
5+// Every CLI entry point has exactly one owner. After the P7 cutover, all
6+// ordinary user-facing operations reach daemons through the per-user
7+// connection broker; narrowly scoped daemon-facing transport helpers stay
8+// infrastructure and never become alternate client façades.
9+//
10+// Ownership rules pinned here:
11+// - Broker-only entries must not keep a direct daemon dialer or a second
12+// host-store writer after cutover. Their CurrentDebt lists today's
13+// direct-connectivity symbols to remove in the same change that
14+// activates the broker owner.
15+// - Broker never owns session persistence. Offline session mutation/list
16+// paths (PersistMutation) become daemon-owned operations reached through
17+// the broker; the persist/snapshot packages stay daemon-side.
18+// - Observation never starts a broker, never undoes an explicit
19+// daemon-stop, and never creates a session attachment.
20+// - Transport helpers terminate transport only. They never own host
21+// configuration, navigation, sessions, or picker state.
22+//
23+// The accompanying test fails when a command kind lacks a row, when a
24+// broker-only row declares no migration debt, when an infra row gains store
25+// or persistence ownership, and when direct-dial, host-store, or offline
26+// persistence symbols appear in files outside the declared debt set.
27+
28+type connectivityOwner uint8
29+
30+const (
31+ // connectivityBrokerOnly marks ordinary user-facing operations that go
32+ // through the broker façade after cutover.
33+ connectivityBrokerOnly connectivityOwner = iota + 1
34+ // connectivityTransportInfra marks narrowly scoped daemon-facing
35+ // transport helpers: process mechanics and carriage terminators that
36+ // must never become client façades.
37+ connectivityTransportInfra
38+ // connectivityLocalOnly marks commands with no daemon or session
39+ // connectivity at all.
40+ connectivityLocalOnly
41+)
42+
43+func (o connectivityOwner) String() string {
44+ switch o {
45+ case connectivityBrokerOnly:
46+ return "broker-only"
47+ case connectivityTransportInfra:
48+ return "transport-infra"
49+ case connectivityLocalOnly:
50+ return "local-only"
51+ default:
52+ return "unknown"
53+ }
54+}
55+
56+// connectivityEntry is one row of the migration matrix. Kind is the cmdKind
57+// identifier from run.go (for example "kindAttach").
58+type connectivityEntry struct {
59+ Kind string
60+ // Summary names the user-visible operation.
61+ Summary string
62+ Owner connectivityOwner
63+ // DirectDialDebt lists today's direct-daemon dial symbols that the
64+ // cutover removes for broker-only entries. Empty for infra/local rows.
65+ DirectDialDebt []string
66+ // HostStoreWrite reports a RemoteHostStore constructor/writer today.
67+ // Only the broker owns the host store after cutover.
68+ HostStoreWrite bool
69+ // PersistMutation reports an offline session persistence read/write
70+ // today. These become daemon-owned operations through the broker.
71+ PersistMutation bool
72+ // Notes records cutover direction and non-obvious boundaries.
73+ Notes string
74+}
75+
76+// connectivityMatrix classifies every cmdKind exactly once.
77+var connectivityMatrix = []connectivityEntry{
78+ {
79+ Kind: "kindAttach",
80+ Summary: "interactive terminal attach/new/ephemeral, incl. remote attach, detached creation, attach preflight",
81+ Owner: connectivityBrokerOnly,
82+ DirectDialDebt: []string{
83+ "localDaemonDialer.Dial (run.go)",
84+ "dialOnlyLocalDialer.Dial inventory control (run.go)",
85+ "ensureDaemonWithLifecycle detached creation (run.go createDetachedLocalSession)",
86+ "registry.ResolveEndpoint remote carriage (run.go runAttachWithDeps)",
87+ "listSessionsWithDialer attach preflight (attach_preflight.go sessionExists)",
88+ },
89+ Notes: "vev without arguments keeps creating an ephemeral session; creation is expressed as client navigation through the broker.",
90+ },
91+ {
92+ Kind: "kindList",
93+ Summary: "session list: local, per-host, and --all",
94+ Owner: connectivityBrokerOnly,
95+ DirectDialDebt: []string{
96+ "realDial listSessionsWithDialer (run.go)",
97+ "waitForDaemonOrLifecycle list path (run.go)",
98+ "RemoteCatalogClient.List over SSH (remote_hosts.go listAllSessions/listOneRemoteHost)",
99+ },
100+ PersistMutation: true,
101+ Notes: "Offline persist.LoadReadOnly list path becomes a daemon-owned operation through the broker; observation never attaches.",
102+ },
103+ {
104+ Kind: "kindHost",
105+ Summary: "host add/rm/list",
106+ Owner: connectivityBrokerOnly,
107+ DirectDialDebt: []string{"remoteHostDeps.hostStore (remote_hosts.go hostAdd/hostRm)"},
108+ HostStoreWrite: true,
109+ Notes: "Second host-store writer today; broker is the sole writer after cutover. Host list is store-only (no dial).",
110+ },
111+ {
112+ Kind: "kindKill",
113+ Summary: "kill one session, kill-all, explicit daemon-stop",
114+ Owner: connectivityBrokerOnly,
115+ DirectDialDebt: []string{
116+ "waitForDaemonOrLifecycle realDial kill path (run.go)",
117+ "forceStopDaemonFallback (force_stop.go)",
118+ },
119+ PersistMutation: true,
120+ Notes: "Offline runOfflineNamedKill mutation becomes daemon-owned via broker. Kill-all purges sessions and leaves the daemon running; daemon-stop is the distinct explicit stop.",
121+ },
122+ {
123+ Kind: "kindCmd",
124+ Summary: "scriptable command requests, incl. remote-catalog",
125+ Owner: connectivityBrokerOnly,
126+ DirectDialDebt: []string{
127+ "realDial command path (cmd.go)",
128+ "ensureDaemonWithLifecycle remote-catalog path (cmd.go)",
129+ },
130+ Notes: "Command tracker semantics unchanged; only the carriage moves behind the broker.",
131+ },
132+ {
133+ Kind: "kindUIDriver",
134+ Summary: "headless UI-driver attach",
135+ Owner: connectivityBrokerOnly,
136+ DirectDialDebt: []string{
137+ "localDaemonDialer launch/local paths (ui_driver.go, run.go runAttachWithDeps)",
138+ "configuredRemoteLaunches dialerFactory (ui_driver.go)",
139+ },
140+ Notes: "Launch-owned remote dialer and cleanup move behind the broker; UI-driver keeps no parallel carriage.",
141+ },
142+ {
143+ Kind: "kindWebDaemon",
144+ Summary: "browser gateway launcher",
145+ Owner: connectivityBrokerOnly,
146+ DirectDialDebt: []string{"gateway session path via runAttachWithDeps (web_daemon.go)"},
147+ Notes: "Launcher mechanics stay local; gateway session traffic goes through the broker like any client.",
148+ },
149+ {
150+ Kind: "kindWebServe",
151+ Summary: "browser gateway server",
152+ Owner: connectivityBrokerOnly,
153+ DirectDialDebt: []string{"gateway session path via runAttachWithDeps (web_daemon.go)"},
154+ Notes: "One client runner per WebSocket; no bypass of the broker façade.",
155+ },
156+ {
157+ Kind: "kindDaemon",
158+ Summary: "daemon process: owns sessions, serves typed connections",
159+ Owner: connectivityTransportInfra,
160+ Notes: "Daemon accepts one typed connection per broker logical stream after cutover. Never aggregates other daemons, never owns the host registry (P7 removal set).",
161+ },
162+ {
163+ Kind: "kindDaemonLauncher",
164+ Summary: "short-lived daemon spawn launcher",
165+ Owner: connectivityTransportInfra,
166+ Notes: "Process mechanics only; never a connectivity façade.",
167+ },
168+ {
169+ Kind: "kindStdio",
170+ Summary: "_stdio SSH-side carriage proxy",
171+ Owner: connectivityTransportInfra,
172+ Notes: "Terminates SSH stdio transport on the far side; never resolves navigation or owns sessions.",
173+ },
174+ {
175+ Kind: "kindQUICBootstrap",
176+ Summary: "_quic-bootstrap SSH-side bootstrap",
177+ Owner: connectivityTransportInfra,
178+ Notes: "One-time authenticated bootstrap; terminates transport, never navigation authority.",
179+ },
180+ {
181+ Kind: "kindQUICProxy",
182+ Summary: "_quic-proxy carriage-neutral bridge",
183+ Owner: connectivityTransportInfra,
184+ Notes: "Blind proxy may forward raw envelopes; exposes no bytes to use cases and owns no sessions.",
185+ },
186+ {
187+ Kind: "kindRemotePreview",
188+ Summary: "_remote-preview SSH-side preview carriage",
189+ Owner: connectivityTransportInfra,
190+ DirectDialDebt: []string{
191+ "ipc.DialContext direct local dial (remote_preview.go)",
192+ },
193+ Notes: "Far-side helper only. Client-facing previews flow through broker snapshots after cutover; this helper never becomes a client façade. Direct-dial debt is recorded here rather than as broker debt because the helper itself stays infra.",
194+ },
195+ {
196+ Kind: "kindUIRemoteCleanup",
197+ Summary: "_ui-cleanup remote-side launch cleanup",
198+ Owner: connectivityTransportInfra,
199+ Notes: "Scoped remote cleanup; never launches observers or brokers recursively.",
200+ },
201+ {
202+ Kind: "kindWebRenew",
203+ Summary: "browser token renewal",
204+ Owner: connectivityLocalOnly,
205+ Notes: "Local control only; no daemon or session connectivity.",
206+ },
207+ {
208+ Kind: "kindHelp",
209+ Summary: "usage text",
210+ Owner: connectivityLocalOnly,
211+ Notes: "No connectivity.",
212+ },
213+ {
214+ Kind: "kindVersion",
215+ Summary: "version line",
216+ Owner: connectivityLocalOnly,
217+ Notes: "No connectivity.",
218+ },
219+}
220+
221+// connectivityOwnerFor returns the matrix owner for a cmdKind identifier.
222+func connectivityOwnerFor(kind string) (connectivityOwner, bool) {
223+ for _, entry := range connectivityMatrix {
224+ if entry.Kind == kind {
225+ return entry.Owner, true
226+ }
227+ }
228+ return 0, false
229+}
@@ -0,0 +1,229 @@
1+package app
2+
3+// Connectivity migration matrix (Plan 001 P1.2).
4+//
5+// Every CLI entry point has exactly one owner. After the P7 cutover, all
6+// ordinary user-facing operations reach daemons through the per-user
7+// connection broker; narrowly scoped daemon-facing transport helpers stay
8+// infrastructure and never become alternate client façades.
9+//
10+// Ownership rules pinned here:
11+// - Broker-only entries must not keep a direct daemon dialer or a second
12+// host-store writer after cutover. Their CurrentDebt lists today's
13+// direct-connectivity symbols to remove in the same change that
14+// activates the broker owner.
15+// - Broker never owns session persistence. Offline session mutation/list
16+// paths (PersistMutation) become daemon-owned operations reached through
17+// the broker; the persist/snapshot packages stay daemon-side.
18+// - Observation never starts a broker, never undoes an explicit
19+// daemon-stop, and never creates a session attachment.
20+// - Transport helpers terminate transport only. They never own host
21+// configuration, navigation, sessions, or picker state.
22+//
23+// The accompanying test fails when a command kind lacks a row, when a
24+// broker-only row declares no migration debt, when an infra row gains store
25+// or persistence ownership, and when direct-dial, host-store, or offline
26+// persistence symbols appear in files outside the declared debt set.
27+
28+type connectivityOwner uint8
29+
30+const (
31+ // connectivityBrokerOnly marks ordinary user-facing operations that go
32+ // through the broker façade after cutover.
33+ connectivityBrokerOnly connectivityOwner = iota + 1
34+ // connectivityTransportInfra marks narrowly scoped daemon-facing
35+ // transport helpers: process mechanics and carriage terminators that
36+ // must never become client façades.
37+ connectivityTransportInfra
38+ // connectivityLocalOnly marks commands with no daemon or session
39+ // connectivity at all.
40+ connectivityLocalOnly
41+)
42+
43+func (o connectivityOwner) String() string {
44+ switch o {
45+ case connectivityBrokerOnly:
46+ return "broker-only"
47+ case connectivityTransportInfra:
48+ return "transport-infra"
49+ case connectivityLocalOnly:
50+ return "local-only"
51+ default:
52+ return "unknown"
53+ }
54+}
55+
56+// connectivityEntry is one row of the migration matrix. Kind is the cmdKind
57+// identifier from run.go (for example "kindAttach").
58+type connectivityEntry struct {
59+ Kind string
60+ // Summary names the user-visible operation.
61+ Summary string
62+ Owner connectivityOwner
63+ // DirectDialDebt lists today's direct-daemon dial symbols that the
64+ // cutover removes for broker-only entries. Empty for infra/local rows.
65+ DirectDialDebt []string
66+ // HostStoreWrite reports a RemoteHostStore constructor/writer today.
67+ // Only the broker owns the host store after cutover.
68+ HostStoreWrite bool
69+ // PersistMutation reports an offline session persistence read/write
70+ // today. These become daemon-owned operations through the broker.
71+ PersistMutation bool
72+ // Notes records cutover direction and non-obvious boundaries.
73+ Notes string
74+}
75+
76+// connectivityMatrix classifies every cmdKind exactly once.
77+var connectivityMatrix = []connectivityEntry{
78+ {
79+ Kind: "kindAttach",
80+ Summary: "interactive terminal attach/new/ephemeral, incl. remote attach, detached creation, attach preflight",
81+ Owner: connectivityBrokerOnly,
82+ DirectDialDebt: []string{
83+ "localDaemonDialer.Dial (run.go)",
84+ "dialOnlyLocalDialer.Dial inventory control (run.go)",
85+ "ensureDaemonWithLifecycle detached creation (run.go createDetachedLocalSession)",
86+ "registry.ResolveEndpoint remote carriage (run.go runAttachWithDeps)",
87+ "listSessionsWithDialer attach preflight (attach_preflight.go sessionExists)",
88+ },
89+ Notes: "vev without arguments keeps creating an ephemeral session; creation is expressed as client navigation through the broker.",
90+ },
91+ {
92+ Kind: "kindList",
93+ Summary: "session list: local, per-host, and --all",
94+ Owner: connectivityBrokerOnly,
95+ DirectDialDebt: []string{
96+ "realDial listSessionsWithDialer (run.go)",
97+ "waitForDaemonOrLifecycle list path (run.go)",
98+ "RemoteCatalogClient.List over SSH (remote_hosts.go listAllSessions/listOneRemoteHost)",
99+ },
100+ PersistMutation: true,
101+ Notes: "Offline persist.LoadReadOnly list path becomes a daemon-owned operation through the broker; observation never attaches.",
102+ },
103+ {
104+ Kind: "kindHost",
105+ Summary: "host add/rm/list",
106+ Owner: connectivityBrokerOnly,
107+ DirectDialDebt: []string{"remoteHostDeps.hostStore (remote_hosts.go hostAdd/hostRm)"},
108+ HostStoreWrite: true,
109+ Notes: "Second host-store writer today; broker is the sole writer after cutover. Host list is store-only (no dial).",
110+ },
111+ {
112+ Kind: "kindKill",
113+ Summary: "kill one session, kill-all, explicit daemon-stop",
114+ Owner: connectivityBrokerOnly,
115+ DirectDialDebt: []string{
116+ "waitForDaemonOrLifecycle realDial kill path (run.go)",
117+ "forceStopDaemonFallback (force_stop.go)",
118+ },
119+ PersistMutation: true,
120+ Notes: "Offline runOfflineNamedKill mutation becomes daemon-owned via broker. Kill-all purges sessions and leaves the daemon running; daemon-stop is the distinct explicit stop.",
121+ },
122+ {
123+ Kind: "kindCmd",
124+ Summary: "scriptable command requests, incl. remote-catalog",
125+ Owner: connectivityBrokerOnly,
126+ DirectDialDebt: []string{
127+ "realDial command path (cmd.go)",
128+ "ensureDaemonWithLifecycle remote-catalog path (cmd.go)",
129+ },
130+ Notes: "Command tracker semantics unchanged; only the carriage moves behind the broker.",
131+ },
132+ {
133+ Kind: "kindUIDriver",
134+ Summary: "headless UI-driver attach",
135+ Owner: connectivityBrokerOnly,
136+ DirectDialDebt: []string{
137+ "localDaemonDialer launch/local paths (ui_driver.go, run.go runAttachWithDeps)",
138+ "configuredRemoteLaunches dialerFactory (ui_driver.go)",
139+ },
140+ Notes: "Launch-owned remote dialer and cleanup move behind the broker; UI-driver keeps no parallel carriage.",
141+ },
142+ {
143+ Kind: "kindWebDaemon",
144+ Summary: "browser gateway launcher",
145+ Owner: connectivityBrokerOnly,
146+ DirectDialDebt: []string{"gateway session path via runAttachWithDeps (web_daemon.go)"},
147+ Notes: "Launcher mechanics stay local; gateway session traffic goes through the broker like any client.",
148+ },
149+ {
150+ Kind: "kindWebServe",
151+ Summary: "browser gateway server",
152+ Owner: connectivityBrokerOnly,
153+ DirectDialDebt: []string{"gateway session path via runAttachWithDeps (web_daemon.go)"},
154+ Notes: "One client runner per WebSocket; no bypass of the broker façade.",
155+ },
156+ {
157+ Kind: "kindDaemon",
158+ Summary: "daemon process: owns sessions, serves typed connections",
159+ Owner: connectivityTransportInfra,
160+ Notes: "Daemon accepts one typed connection per broker logical stream after cutover. Never aggregates other daemons, never owns the host registry (P7 removal set).",
161+ },
162+ {
163+ Kind: "kindDaemonLauncher",
164+ Summary: "short-lived daemon spawn launcher",
165+ Owner: connectivityTransportInfra,
166+ Notes: "Process mechanics only; never a connectivity façade.",
167+ },
168+ {
169+ Kind: "kindStdio",
170+ Summary: "_stdio SSH-side carriage proxy",
171+ Owner: connectivityTransportInfra,
172+ Notes: "Terminates SSH stdio transport on the far side; never resolves navigation or owns sessions.",
173+ },
174+ {
175+ Kind: "kindQUICBootstrap",
176+ Summary: "_quic-bootstrap SSH-side bootstrap",
177+ Owner: connectivityTransportInfra,
178+ Notes: "One-time authenticated bootstrap; terminates transport, never navigation authority.",
179+ },
180+ {
181+ Kind: "kindQUICProxy",
182+ Summary: "_quic-proxy carriage-neutral bridge",
183+ Owner: connectivityTransportInfra,
184+ Notes: "Blind proxy may forward raw envelopes; exposes no bytes to use cases and owns no sessions.",
185+ },
186+ {
187+ Kind: "kindRemotePreview",
188+ Summary: "_remote-preview SSH-side preview carriage",
189+ Owner: connectivityTransportInfra,
190+ DirectDialDebt: []string{
191+ "ipc.DialContext direct local dial (remote_preview.go)",
192+ },
193+ Notes: "Far-side helper only. Client-facing previews flow through broker snapshots after cutover; this helper never becomes a client façade. Direct-dial debt is recorded here rather than as broker debt because the helper itself stays infra.",
194+ },
195+ {
196+ Kind: "kindUIRemoteCleanup",
197+ Summary: "_ui-cleanup remote-side launch cleanup",
198+ Owner: connectivityTransportInfra,
199+ Notes: "Scoped remote cleanup; never launches observers or brokers recursively.",
200+ },
201+ {
202+ Kind: "kindWebRenew",
203+ Summary: "browser token renewal",
204+ Owner: connectivityLocalOnly,
205+ Notes: "Local control only; no daemon or session connectivity.",
206+ },
207+ {
208+ Kind: "kindHelp",
209+ Summary: "usage text",
210+ Owner: connectivityLocalOnly,
211+ Notes: "No connectivity.",
212+ },
213+ {
214+ Kind: "kindVersion",
215+ Summary: "version line",
216+ Owner: connectivityLocalOnly,
217+ Notes: "No connectivity.",
218+ },
219+}
220+
221+// connectivityOwnerFor returns the matrix owner for a cmdKind identifier.
222+func connectivityOwnerFor(kind string) (connectivityOwner, bool) {
223+ for _, entry := range connectivityMatrix {
224+ if entry.Kind == kind {
225+ return entry.Owner, true
226+ }
227+ }
228+ return 0, false
229+}
A internal/app/connectivity_matrix_test.go +238 -0
@@ -0,0 +1,238 @@
1+package app
2+
3+import (
4+ "go/ast"
5+ "go/parser"
6+ "go/token"
7+ "os"
8+ "path/filepath"
9+ "regexp"
10+ "sort"
11+ "strings"
12+ "testing"
13+
14+ "github.com/stretchr/testify/require"
15+)
16+
17+// TestConnectivityMatrixCoversEveryCommandKind pins P1.2: every cmdKind
18+// constant declared in run.go has exactly one matrix row. A new CLI entry
19+// point without a row fails here before any transport work can ignore it.
20+func TestConnectivityMatrixCoversEveryCommandKind(t *testing.T) {
21+ t.Parallel()
22+
23+ kinds := commandKindsFromRunGo(t)
24+ require.NotEmpty(t, kinds, "run.go must declare cmdKind constants")
25+
26+ seen := make(map[string]int, len(connectivityMatrix))
27+ for _, entry := range connectivityMatrix {
28+ require.NotEmpty(t, entry.Kind, "matrix row must name a cmdKind")
29+ require.NotEmpty(t, entry.Summary, "matrix row %q must summarize the operation", entry.Kind)
30+ require.NotEmpty(t, entry.Notes, "matrix row %q must record cutover direction", entry.Kind)
31+ seen[entry.Kind]++
32+ }
33+ for kind, count := range seen {
34+ require.Equal(t, 1, count, "matrix row %q must appear exactly once", kind)
35+ }
36+ var missing, extra []string
37+ for _, kind := range kinds {
38+ if _, ok := seen[kind]; !ok {
39+ missing = append(missing, kind)
40+ }
41+ }
42+ kindSet := make(map[string]struct{}, len(kinds))
43+ for _, kind := range kinds {
44+ kindSet[kind] = struct{}{}
45+ }
46+ for kind := range seen {
47+ if _, ok := kindSet[kind]; !ok {
48+ extra = append(extra, kind)
49+ }
50+ }
51+ sort.Strings(missing)
52+ sort.Strings(extra)
53+ require.Empty(t, missing, "matrix is missing cmdKind rows")
54+ require.Empty(t, extra, "matrix references unknown cmdKind rows")
55+
56+ owner, ok := connectivityOwnerFor("kindAttach")
57+ require.True(t, ok, "matrix lookup must resolve known kinds")
58+ require.Equal(t, connectivityBrokerOnly, owner)
59+ _, ok = connectivityOwnerFor("kindDoesNotExist")
60+ require.False(t, ok, "matrix lookup must reject unknown kinds")
61+}
62+
63+// TestConnectivityMatrixOwnershipRules pins the P1.2 ownership invariants:
64+// broker-only rows must declare migration debt, infra rows must never gain
65+// store or persistence ownership, and local-only rows carry no debt.
66+func TestConnectivityMatrixOwnershipRules(t *testing.T) {
67+ t.Parallel()
68+
69+ tests := make([]struct {
70+ name string
71+ entry connectivityEntry
72+ }, 0, len(connectivityMatrix))
73+ for _, entry := range connectivityMatrix {
74+ tests = append(tests, struct {
75+ name string
76+ entry connectivityEntry
77+ }{name: entry.Kind, entry: entry})
78+ }
79+ for _, tt := range tests {
80+ tt := tt
81+ t.Run(tt.name, func(t *testing.T) {
82+ t.Parallel()
83+ entry := tt.entry
84+ switch entry.Owner {
85+ case connectivityBrokerOnly:
86+ hasDebt := len(entry.DirectDialDebt) > 0 || entry.HostStoreWrite || entry.PersistMutation
87+ require.True(t, hasDebt, "broker-only %q must declare migration debt (direct dial, host-store write, or persist mutation)", entry.Kind)
88+ case connectivityTransportInfra:
89+ require.False(t, entry.HostStoreWrite, "infra %q must never own the host store", entry.Kind)
90+ require.False(t, entry.PersistMutation, "infra %q must never own session persistence", entry.Kind)
91+ if len(entry.DirectDialDebt) > 0 {
92+ require.NotEmpty(t, entry.Notes, "infra %q with local dial debt must justify why it stays infra", entry.Kind)
93+ }
94+ case connectivityLocalOnly:
95+ require.Empty(t, entry.DirectDialDebt, "local-only %q must carry no dial debt", entry.Kind)
96+ require.False(t, entry.HostStoreWrite, "local-only %q must never own the host store", entry.Kind)
97+ require.False(t, entry.PersistMutation, "local-only %q must never own session persistence", entry.Kind)
98+ default:
99+ t.Fatalf("row %q has unknown owner %v", entry.Kind, entry.Owner)
100+ }
101+ for _, debt := range entry.DirectDialDebt {
102+ require.NotEmpty(t, debt, "row %q has an empty debt entry", entry.Kind)
103+ }
104+ })
105+ }
106+}
107+
108+// TestConnectivityMatrixDebtFilesExist keeps the matrix honest: every file
109+// named in a debt entry must exist under internal/app.
110+func TestConnectivityMatrixDebtFilesExist(t *testing.T) {
111+ t.Parallel()
112+
113+ fileRef := regexp.MustCompile(`([a-z_]+\.go)`)
114+ for _, entry := range connectivityMatrix {
115+ for _, debt := range entry.DirectDialDebt {
116+ for _, match := range fileRef.FindAllStringSubmatch(debt, -1) {
117+ path := filepath.Join("internal", "app", match[1])
118+ // Tests run with the package dir as CWD, so resolve from there.
119+ if _, err := os.Stat(match[1]); err == nil {
120+ continue
121+ }
122+ if _, err := os.Stat(path); err == nil {
123+ continue
124+ }
125+ t.Errorf("row %q references missing file %q (debt %q)", entry.Kind, match[1], debt)
126+ }
127+ }
128+ }
129+}
130+
131+// TestConnectivityMatrixCurrentDebtPresent pins today's direct-connectivity
132+// debt so the P7 cutover can find every site it must remove in the same
133+// change that activates the broker owner. If any of these symbols disappear,
134+// update the matrix in the same change.
135+func TestConnectivityMatrixCurrentDebtPresent(t *testing.T) {
136+ t.Parallel()
137+
138+ sources := loadAppSources(t, false)
139+
140+ assertContains := func(symbol, file string) {
141+ t.Helper()
142+ body, ok := sources[file]
143+ require.True(t, ok, "expected app source file %q", file)
144+ require.Contains(t, body, symbol, "expected debt symbol %q in %q", symbol, file)
145+ }
146+
147+ assertContains("type localDaemonDialer struct", "run.go")
148+ assertContains("type dialOnlyLocalDialer struct", "run.go")
149+ assertContains("func (d localDaemonDialer) Dial", "run.go")
150+ assertContains("func (d dialOnlyLocalDialer) Dial", "run.go")
151+ assertContains("func ensureDaemonWithLifecycle", "spawn.go")
152+ assertContains("func waitForDaemonOrLifecycle", "spawn.go")
153+ assertContains("func realDial", "spawn.go")
154+ assertContains("func realSpawn", "spawn.go")
155+ assertContains("func (d remoteHostDeps) hostStore", "remote_hosts.go")
156+ assertContains("func hostAdd", "remote_hosts.go")
157+ assertContains("func hostRm", "remote_hosts.go")
158+ assertContains("func listSessionsWithDialer", "run.go")
159+ assertContains("func sessionExists", "attach_preflight.go")
160+ assertContains("func runOfflineNamedKill", "run.go")
161+ assertContains("persist.LoadReadOnly", "run.go")
162+ assertContains("persist.OpenOrCreate", "run.go")
163+ assertContains("func newClientHostRegistry", "client_hosts.go")
164+}
165+
166+// TestConnectivityMatrixSingleHostStoreWriterSet fails when a new
167+// RemoteHostStore constructor call site appears outside the declared set.
168+// After cutover the broker is the sole writer; until then the matrix must
169+// name every writer so none is missed.
170+func TestConnectivityMatrixSingleHostStoreWriterSet(t *testing.T) {
171+ t.Parallel()
172+
173+ sources := loadAppSources(t, false)
174+ var writers []string
175+ for file, body := range sources {
176+ if strings.Contains(body, "NewFileHostStore") {
177+ writers = append(writers, file)
178+ }
179+ }
180+ sort.Strings(writers)
181+ require.Equal(t, []string{"remote_hosts.go", "run.go"}, writers,
182+ "host-store constructor sites changed; update the matrix debt set in the same change")
183+}
184+
185+// commandKindsFromRunGo extracts the kind* const names from run.go so the
186+// matrix test tracks the real dispatch table instead of a hardcoded copy.
187+func commandKindsFromRunGo(t *testing.T) []string {
188+ t.Helper()
189+
190+ fset := token.NewFileSet()
191+ file, err := parser.ParseFile(fset, "run.go", nil, 0)
192+ require.NoError(t, err, "parse run.go for cmdKind constants")
193+
194+ var kinds []string
195+ for _, decl := range file.Decls {
196+ gen, ok := decl.(*ast.GenDecl)
197+ if !ok || gen.Tok != token.CONST {
198+ continue
199+ }
200+ for _, spec := range gen.Specs {
201+ value, ok := spec.(*ast.ValueSpec)
202+ if !ok {
203+ continue
204+ }
205+ for _, name := range value.Names {
206+ if strings.HasPrefix(name.Name, "kind") {
207+ kinds = append(kinds, name.Name)
208+ }
209+ }
210+ }
211+ }
212+ sort.Strings(kinds)
213+ return kinds
214+}
215+
216+// loadAppSources reads production (non-test) .go files in this package.
217+// When includeTests is false, _test.go files are skipped.
218+func loadAppSources(t *testing.T, includeTests bool) map[string]string {
219+ t.Helper()
220+
221+ entries, err := os.ReadDir(".")
222+ require.NoError(t, err, "read app package dir")
223+ sources := make(map[string]string)
224+ for _, entry := range entries {
225+ name := entry.Name()
226+ if entry.IsDir() || !strings.HasSuffix(name, ".go") {
227+ continue
228+ }
229+ if !includeTests && strings.HasSuffix(name, "_test.go") {
230+ continue
231+ }
232+ raw, err := os.ReadFile(name)
233+ require.NoError(t, err, "read app source %q", name)
234+ sources[name] = string(raw)
235+ }
236+ require.NotEmpty(t, sources, "app sources must not be empty")
237+ return sources
238+}
@@ -0,0 +1,238 @@
1+package app
2+
3+import (
4+ "go/ast"
5+ "go/parser"
6+ "go/token"
7+ "os"
8+ "path/filepath"
9+ "regexp"
10+ "sort"
11+ "strings"
12+ "testing"
13+
14+ "github.com/stretchr/testify/require"
15+)
16+
17+// TestConnectivityMatrixCoversEveryCommandKind pins P1.2: every cmdKind
18+// constant declared in run.go has exactly one matrix row. A new CLI entry
19+// point without a row fails here before any transport work can ignore it.
20+func TestConnectivityMatrixCoversEveryCommandKind(t *testing.T) {
21+ t.Parallel()
22+
23+ kinds := commandKindsFromRunGo(t)
24+ require.NotEmpty(t, kinds, "run.go must declare cmdKind constants")
25+
26+ seen := make(map[string]int, len(connectivityMatrix))
27+ for _, entry := range connectivityMatrix {
28+ require.NotEmpty(t, entry.Kind, "matrix row must name a cmdKind")
29+ require.NotEmpty(t, entry.Summary, "matrix row %q must summarize the operation", entry.Kind)
30+ require.NotEmpty(t, entry.Notes, "matrix row %q must record cutover direction", entry.Kind)
31+ seen[entry.Kind]++
32+ }
33+ for kind, count := range seen {
34+ require.Equal(t, 1, count, "matrix row %q must appear exactly once", kind)
35+ }
36+ var missing, extra []string
37+ for _, kind := range kinds {
38+ if _, ok := seen[kind]; !ok {
39+ missing = append(missing, kind)
40+ }
41+ }
42+ kindSet := make(map[string]struct{}, len(kinds))
43+ for _, kind := range kinds {
44+ kindSet[kind] = struct{}{}
45+ }
46+ for kind := range seen {
47+ if _, ok := kindSet[kind]; !ok {
48+ extra = append(extra, kind)
49+ }
50+ }
51+ sort.Strings(missing)
52+ sort.Strings(extra)
53+ require.Empty(t, missing, "matrix is missing cmdKind rows")
54+ require.Empty(t, extra, "matrix references unknown cmdKind rows")
55+
56+ owner, ok := connectivityOwnerFor("kindAttach")
57+ require.True(t, ok, "matrix lookup must resolve known kinds")
58+ require.Equal(t, connectivityBrokerOnly, owner)
59+ _, ok = connectivityOwnerFor("kindDoesNotExist")
60+ require.False(t, ok, "matrix lookup must reject unknown kinds")
61+}
62+
63+// TestConnectivityMatrixOwnershipRules pins the P1.2 ownership invariants:
64+// broker-only rows must declare migration debt, infra rows must never gain
65+// store or persistence ownership, and local-only rows carry no debt.
66+func TestConnectivityMatrixOwnershipRules(t *testing.T) {
67+ t.Parallel()
68+
69+ tests := make([]struct {
70+ name string
71+ entry connectivityEntry
72+ }, 0, len(connectivityMatrix))
73+ for _, entry := range connectivityMatrix {
74+ tests = append(tests, struct {
75+ name string
76+ entry connectivityEntry
77+ }{name: entry.Kind, entry: entry})
78+ }
79+ for _, tt := range tests {
80+ tt := tt
81+ t.Run(tt.name, func(t *testing.T) {
82+ t.Parallel()
83+ entry := tt.entry
84+ switch entry.Owner {
85+ case connectivityBrokerOnly:
86+ hasDebt := len(entry.DirectDialDebt) > 0 || entry.HostStoreWrite || entry.PersistMutation
87+ require.True(t, hasDebt, "broker-only %q must declare migration debt (direct dial, host-store write, or persist mutation)", entry.Kind)
88+ case connectivityTransportInfra:
89+ require.False(t, entry.HostStoreWrite, "infra %q must never own the host store", entry.Kind)
90+ require.False(t, entry.PersistMutation, "infra %q must never own session persistence", entry.Kind)
91+ if len(entry.DirectDialDebt) > 0 {
92+ require.NotEmpty(t, entry.Notes, "infra %q with local dial debt must justify why it stays infra", entry.Kind)
93+ }
94+ case connectivityLocalOnly:
95+ require.Empty(t, entry.DirectDialDebt, "local-only %q must carry no dial debt", entry.Kind)
96+ require.False(t, entry.HostStoreWrite, "local-only %q must never own the host store", entry.Kind)
97+ require.False(t, entry.PersistMutation, "local-only %q must never own session persistence", entry.Kind)
98+ default:
99+ t.Fatalf("row %q has unknown owner %v", entry.Kind, entry.Owner)
100+ }
101+ for _, debt := range entry.DirectDialDebt {
102+ require.NotEmpty(t, debt, "row %q has an empty debt entry", entry.Kind)
103+ }
104+ })
105+ }
106+}
107+
108+// TestConnectivityMatrixDebtFilesExist keeps the matrix honest: every file
109+// named in a debt entry must exist under internal/app.
110+func TestConnectivityMatrixDebtFilesExist(t *testing.T) {
111+ t.Parallel()
112+
113+ fileRef := regexp.MustCompile(`([a-z_]+\.go)`)
114+ for _, entry := range connectivityMatrix {
115+ for _, debt := range entry.DirectDialDebt {
116+ for _, match := range fileRef.FindAllStringSubmatch(debt, -1) {
117+ path := filepath.Join("internal", "app", match[1])
118+ // Tests run with the package dir as CWD, so resolve from there.
119+ if _, err := os.Stat(match[1]); err == nil {
120+ continue
121+ }
122+ if _, err := os.Stat(path); err == nil {
123+ continue
124+ }
125+ t.Errorf("row %q references missing file %q (debt %q)", entry.Kind, match[1], debt)
126+ }
127+ }
128+ }
129+}
130+
131+// TestConnectivityMatrixCurrentDebtPresent pins today's direct-connectivity
132+// debt so the P7 cutover can find every site it must remove in the same
133+// change that activates the broker owner. If any of these symbols disappear,
134+// update the matrix in the same change.
135+func TestConnectivityMatrixCurrentDebtPresent(t *testing.T) {
136+ t.Parallel()
137+
138+ sources := loadAppSources(t, false)
139+
140+ assertContains := func(symbol, file string) {
141+ t.Helper()
142+ body, ok := sources[file]
143+ require.True(t, ok, "expected app source file %q", file)
144+ require.Contains(t, body, symbol, "expected debt symbol %q in %q", symbol, file)
145+ }
146+
147+ assertContains("type localDaemonDialer struct", "run.go")
148+ assertContains("type dialOnlyLocalDialer struct", "run.go")
149+ assertContains("func (d localDaemonDialer) Dial", "run.go")
150+ assertContains("func (d dialOnlyLocalDialer) Dial", "run.go")
151+ assertContains("func ensureDaemonWithLifecycle", "spawn.go")
152+ assertContains("func waitForDaemonOrLifecycle", "spawn.go")
153+ assertContains("func realDial", "spawn.go")
154+ assertContains("func realSpawn", "spawn.go")
155+ assertContains("func (d remoteHostDeps) hostStore", "remote_hosts.go")
156+ assertContains("func hostAdd", "remote_hosts.go")
157+ assertContains("func hostRm", "remote_hosts.go")
158+ assertContains("func listSessionsWithDialer", "run.go")
159+ assertContains("func sessionExists", "attach_preflight.go")
160+ assertContains("func runOfflineNamedKill", "run.go")
161+ assertContains("persist.LoadReadOnly", "run.go")
162+ assertContains("persist.OpenOrCreate", "run.go")
163+ assertContains("func newClientHostRegistry", "client_hosts.go")
164+}
165+
166+// TestConnectivityMatrixSingleHostStoreWriterSet fails when a new
167+// RemoteHostStore constructor call site appears outside the declared set.
168+// After cutover the broker is the sole writer; until then the matrix must
169+// name every writer so none is missed.
170+func TestConnectivityMatrixSingleHostStoreWriterSet(t *testing.T) {
171+ t.Parallel()
172+
173+ sources := loadAppSources(t, false)
174+ var writers []string
175+ for file, body := range sources {
176+ if strings.Contains(body, "NewFileHostStore") {
177+ writers = append(writers, file)
178+ }
179+ }
180+ sort.Strings(writers)
181+ require.Equal(t, []string{"remote_hosts.go", "run.go"}, writers,
182+ "host-store constructor sites changed; update the matrix debt set in the same change")
183+}
184+
185+// commandKindsFromRunGo extracts the kind* const names from run.go so the
186+// matrix test tracks the real dispatch table instead of a hardcoded copy.
187+func commandKindsFromRunGo(t *testing.T) []string {
188+ t.Helper()
189+
190+ fset := token.NewFileSet()
191+ file, err := parser.ParseFile(fset, "run.go", nil, 0)
192+ require.NoError(t, err, "parse run.go for cmdKind constants")
193+
194+ var kinds []string
195+ for _, decl := range file.Decls {
196+ gen, ok := decl.(*ast.GenDecl)
197+ if !ok || gen.Tok != token.CONST {
198+ continue
199+ }
200+ for _, spec := range gen.Specs {
201+ value, ok := spec.(*ast.ValueSpec)
202+ if !ok {
203+ continue
204+ }
205+ for _, name := range value.Names {
206+ if strings.HasPrefix(name.Name, "kind") {
207+ kinds = append(kinds, name.Name)
208+ }
209+ }
210+ }
211+ }
212+ sort.Strings(kinds)
213+ return kinds
214+}
215+
216+// loadAppSources reads production (non-test) .go files in this package.
217+// When includeTests is false, _test.go files are skipped.
218+func loadAppSources(t *testing.T, includeTests bool) map[string]string {
219+ t.Helper()
220+
221+ entries, err := os.ReadDir(".")
222+ require.NoError(t, err, "read app package dir")
223+ sources := make(map[string]string)
224+ for _, entry := range entries {
225+ name := entry.Name()
226+ if entry.IsDir() || !strings.HasSuffix(name, ".go") {
227+ continue
228+ }
229+ if !includeTests && strings.HasSuffix(name, "_test.go") {
230+ continue
231+ }
232+ raw, err := os.ReadFile(name)
233+ require.NoError(t, err, "read app source %q", name)
234+ sources[name] = string(raw)
235+ }
236+ require.NotEmpty(t, sources, "app sources must not be empty")
237+ return sources
238+}
A internal/ports/broker.go +641 -0
@@ -0,0 +1,641 @@
1+package ports
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "strings"
8+ "unicode"
9+ "unicode/utf8"
10+
11+ "github.com/bnema/vev/internal/domain"
12+ "github.com/bnema/vev/internal/protocol"
13+ "github.com/bnema/vev/internal/protocol/catalogue"
14+)
15+
16+// Broker semantic contracts (Plan 001 P1.3).
17+//
18+// The broker is the machine-wide owner of connectivity: configured hosts,
19+// daemon reachability and catalogue snapshots, pooled physical transports,
20+// and multiplexed logical streams. It never owns picker UI, navigation
21+// decisions, sessions, PTYs, terminal geometry, or attachment semantics.
22+//
23+// Identity levels stay distinct:
24+// - configured host registration/incarnation (domain.RemoteRegistration)
25+// - authenticated daemon identity used for pooling (BrokerDaemonIdentity)
26+// - daemon process incarnation used to detect restart
27+// (BrokerDaemonIncarnation)
28+// - exact session lifecycle identity (domain.SessionLifecycleID via
29+// protocol.ExactSessionTarget)
30+// - broker epoch/revision (BrokerEpoch/BrokerRevision)
31+// - client connection identity (BrokerConnectionID)
32+// - logical stream identity (BrokerStreamID)
33+//
34+// An epoch change forces a full resynchronization: stream IDs and events
35+// are scoped to one client connection and broker epoch, so stale
36+// completions cannot revive an attachment. Tombstones and registration
37+// incarnation fence remove/re-add races independently of the epoch.
38+//
39+// Raw frames, codecs, environment policy enforcement, and worker
40+// implementations live outside this package. Per-request session
41+// environment (BrokerOpenStreamRequest.Env) stays separate from the broker
42+// process environment.
43+
44+const (
45+ // BrokerMaxHosts bounds one snapshot publication. It mirrors the
46+ // catalogue host budget so a broker snapshot never exceeds what
47+ // discovery can produce.
48+ BrokerMaxHosts = catalogue.RemoteCatalogMaxHosts
49+ // BrokerMaxSessionsPerHost bounds sessions carried per host in one
50+ // snapshot publication.
51+ BrokerMaxSessionsPerHost = catalogue.RemoteCatalogMaxSessions
52+ // BrokerMaxTombstones bounds retired registrations carried alongside
53+ // one snapshot for remove/re-add fencing.
54+ BrokerMaxTombstones = catalogue.RemoteCatalogMaxHosts
55+ // BrokerMaxErrorBytes bounds human-readable error text in broker
56+ // results. The underlying cause stays behind errors.Is/As where the
57+ // contract carries an error value.
58+ BrokerMaxErrorBytes = 512
59+ // BrokerMaxEnvEntries bounds per-request environment entries. The
60+ // broker never merges these into its own process environment.
61+ BrokerMaxEnvEntries = 128
62+ // BrokerMaxEnvEntryBytes bounds one per-request environment entry.
63+ BrokerMaxEnvEntryBytes = 4 << 10
64+ // BrokerMaxTransportBytes bounds the opaque transport policy token.
65+ BrokerMaxTransportBytes = 64
66+ // BrokerMaxIdentityBytes bounds the authenticated daemon identity token.
67+ BrokerMaxIdentityBytes = 256
68+)
69+
70+// BrokerEpoch identifies one broker process incarnation. Every broker
71+// process samples a fresh non-zero epoch at startup; every publication
72+// carries a revision within that epoch. Clients discard all broker state
73+// and resubscribe when the epoch changes.
74+type BrokerEpoch uint64
75+
76+// BrokerRevision is monotonic within one broker epoch. Revisions never
77+// compare across epochs.
78+type BrokerRevision uint64
79+
80+// BrokerConnectionID identifies one client connection to the broker,
81+// scoped to a broker epoch. It never identifies a session or a stream.
82+type BrokerConnectionID [16]byte
83+
84+// BrokerStreamID identifies one logical stream within a client connection
85+// and broker epoch. It never crosses connections or epochs.
86+type BrokerStreamID uint64
87+
88+// BrokerOperationID is the idempotency identity for one mutating broker
89+// operation. Clients refresh authoritative state on outcome-unknown and
90+// never blindly replay a non-idempotent operation.
91+type BrokerOperationID [16]byte
92+
93+// BrokerDaemonIncarnation identifies one daemon process incarnation and
94+// detects restarts behind a stable authenticated identity.
95+type BrokerDaemonIncarnation [16]byte
96+
97+func (id BrokerConnectionID) IsZero() bool { return id == (BrokerConnectionID{}) }
98+
99+func (id BrokerConnectionID) Validate() error {
100+ if id.IsZero() {
101+ return errors.New("ports: broker connection ID is zero")
102+ }
103+ return nil
104+}
105+
106+func (id BrokerStreamID) Validate() error {
107+ if id == 0 {
108+ return errors.New("ports: broker stream ID is zero")
109+ }
110+ return nil
111+}
112+
113+func (id BrokerOperationID) IsZero() bool { return id == (BrokerOperationID{}) }
114+
115+func (id BrokerOperationID) Validate() error {
116+ if id.IsZero() {
117+ return errors.New("ports: broker operation ID is zero")
118+ }
119+ return nil
120+}
121+
122+func (id BrokerDaemonIncarnation) IsZero() bool { return id == (BrokerDaemonIncarnation{}) }
123+
124+func (id BrokerDaemonIncarnation) Validate() error {
125+ if id.IsZero() {
126+ return errors.New("ports: broker daemon incarnation is zero")
127+ }
128+ return nil
129+}
130+
131+// BrokerDaemonIdentity is the authenticated daemon/service identity used
132+// for physical transport pooling. Display aliases are never pooled: two
133+// endpoints share a transport only after authenticated identity and every
134+// trust, launch, isolation, and transport policy compare compatible.
135+type BrokerDaemonIdentity string
136+
137+func (id BrokerDaemonIdentity) Validate() error {
138+ return validateBrokerToken(string(id), BrokerMaxIdentityBytes, "daemon identity")
139+}
140+
141+// BrokerPolicy is the exact connection policy required before two
142+// endpoints may share a pooled physical transport. Pooling requires exact
143+// equality: conflicting policy is rejected, never merged or inherited
144+// from the first launching client.
145+type BrokerPolicy struct {
146+ ProtocolVersion uint16
147+ CatalogSchemaVersion uint16
148+ EnvironmentPolicy protocol.EnvironmentPolicy
149+ Transport string
150+}
151+
152+func (p BrokerPolicy) Validate() error {
153+ if p.ProtocolVersion == 0 {
154+ return errors.New("ports: broker policy has no protocol version")
155+ }
156+ if p.CatalogSchemaVersion == 0 {
157+ return errors.New("ports: broker policy has no catalog schema version")
158+ }
159+ if err := protocol.ValidateEnvironmentPolicy(p.EnvironmentPolicy); err != nil {
160+ return fmt.Errorf("ports: broker policy: %w", err)
161+ }
162+ if err := validateBrokerToken(p.Transport, BrokerMaxTransportBytes, "policy transport"); err != nil {
163+ return err
164+ }
165+ return nil
166+}
167+
168+// Compatible reports whether two validated policies may share a pooled
169+// physical transport. It requires exact equality across every field.
170+func (p BrokerPolicy) Compatible(other BrokerPolicy) bool {
171+ return p.ProtocolVersion == other.ProtocolVersion &&
172+ p.CatalogSchemaVersion == other.CatalogSchemaVersion &&
173+ p.EnvironmentPolicy == other.EnvironmentPolicy &&
174+ p.Transport == other.Transport
175+}
176+
177+// BrokerHostTombstone fences a removed host registration so a stale probe,
178+// snapshot, or completion bound to the retired incarnation cannot overwrite
179+// or revive current authority. A re-added endpoint carries a new
180+// incarnation and is never fenced by an older tombstone.
181+type BrokerHostTombstone struct {
182+ Endpoint string
183+ Registration domain.RemoteRegistration
184+ RetiredRevision BrokerRevision
185+}
186+
187+func (t BrokerHostTombstone) Validate() error {
188+ if err := domain.ValidateRemoteHostTarget(t.Endpoint); err != nil {
189+ return fmt.Errorf("ports: broker tombstone: %w", err)
190+ }
191+ if err := t.Registration.Validate(); err != nil {
192+ return fmt.Errorf("ports: broker tombstone: %w", err)
193+ }
194+ if t.Endpoint != t.Registration.Endpoint {
195+ return errors.New("ports: broker tombstone endpoint does not match registration")
196+ }
197+ if t.RetiredRevision == 0 {
198+ return errors.New("ports: broker tombstone has no retired revision")
199+ }
200+ return nil
201+}
202+
203+// Fences reports whether this tombstone retires the supplied registration:
204+// same endpoint and incarnation, with a retired generation at or past the
205+// candidate. A re-added endpoint with a fresh incarnation is never fenced.
206+func (t BrokerHostTombstone) Fences(candidate domain.RemoteRegistration) bool {
207+ return t.Endpoint == candidate.Endpoint &&
208+ t.Registration.Incarnation == candidate.Incarnation &&
209+ t.Registration.Generation >= candidate.Generation
210+}
211+
212+// BrokerSnapshot is an immutable, fully defensive broker publication. Hosts
213+// reuse the ordered RemoteHostSnapshot projection; Removed carries the
214+// bounded tombstone set that fences stale authority. Nested slices are
215+// never mutated after publication.
216+type BrokerSnapshot struct {
217+ Epoch BrokerEpoch
218+ Revision BrokerRevision
219+ Hosts []RemoteHostSnapshot
220+ Removed []BrokerHostTombstone
221+}
222+
223+func (s BrokerSnapshot) Validate() error {
224+ if s.Epoch == 0 {
225+ return errors.New("ports: broker snapshot has no epoch")
226+ }
227+ if s.Revision == 0 {
228+ return errors.New("ports: broker snapshot has no revision")
229+ }
230+ if len(s.Hosts) > BrokerMaxHosts {
231+ return errors.New("ports: broker snapshot has too many hosts")
232+ }
233+ if len(s.Removed) > BrokerMaxTombstones {
234+ return errors.New("ports: broker snapshot has too many tombstones")
235+ }
236+ seen := make(map[string]struct{}, len(s.Hosts))
237+ for _, host := range s.Hosts {
238+ if err := domain.ValidateRemoteHostTarget(host.Endpoint); err != nil {
239+ return fmt.Errorf("ports: broker snapshot: %w", err)
240+ }
241+ if err := host.Registration.Validate(); err != nil {
242+ return fmt.Errorf("ports: broker snapshot: %w", err)
243+ }
244+ if host.Endpoint != host.Registration.Endpoint {
245+ return errors.New("ports: broker snapshot host endpoint does not match registration")
246+ }
247+ if len(host.Sessions) > BrokerMaxSessionsPerHost {
248+ return errors.New("ports: broker snapshot host has too many sessions")
249+ }
250+ if _, dup := seen[host.Endpoint]; dup {
251+ return fmt.Errorf("ports: broker snapshot has duplicate host %q", host.Endpoint)
252+ }
253+ seen[host.Endpoint] = struct{}{}
254+ }
255+ for _, tombstone := range s.Removed {
256+ if err := tombstone.Validate(); err != nil {
257+ return err
258+ }
259+ if _, live := seen[tombstone.Endpoint]; live {
260+ return fmt.Errorf("ports: broker snapshot carries live host %q as tombstone", tombstone.Endpoint)
261+ }
262+ }
263+ return nil
264+}
265+
266+// Clone returns a defensive copy with independent host, session, tab, and
267+// tombstone slices.
268+func (s BrokerSnapshot) Clone() BrokerSnapshot {
269+ out := s
270+ out.Hosts = make([]RemoteHostSnapshot, len(s.Hosts))
271+ for i, host := range s.Hosts {
272+ out.Hosts[i] = host.Clone()
273+ }
274+ out.Removed = append([]BrokerHostTombstone(nil), s.Removed...)
275+ return out
276+}
277+
278+// Find returns the snapshot for an exact endpoint identity.
279+func (s BrokerSnapshot) Find(endpoint string) (RemoteHostSnapshot, bool) {
280+ for _, host := range s.Hosts {
281+ if host.Endpoint == endpoint {
282+ return host, true
283+ }
284+ }
285+ return RemoteHostSnapshot{}, false
286+}
287+
288+// Supersedes reports whether s is newer than other. Any epoch change
289+// forces a full resynchronization: a snapshot with a valid epoch
290+// supersedes every snapshot from another epoch (or none), regardless of
291+// numeric order, because epochs are fresh per broker process and never
292+// ordered across processes. Within one epoch the higher revision wins.
293+// Revisions never compare across epochs. A zero-epoch snapshot is
294+// invalid and never supersedes.
295+func (s BrokerSnapshot) Supersedes(other BrokerSnapshot) bool {
296+ if s.Epoch == 0 {
297+ return false
298+ }
299+ if s.Epoch != other.Epoch {
300+ return true
301+ }
302+ return s.Revision > other.Revision
303+}
304+
305+// BrokerCompletionIsStale reports whether a probe or operation completion
306+// must be rejected: a different broker epoch, or a registration that no
307+// longer exactly matches current authority (endpoint, incarnation, and
308+// generation must all match).
309+func BrokerCompletionIsStale(currentRegistration domain.RemoteRegistration, currentEpoch BrokerEpoch, completionRegistration domain.RemoteRegistration, completionEpoch BrokerEpoch) bool {
310+ if currentEpoch == 0 || completionEpoch == 0 {
311+ return true
312+ }
313+ if currentEpoch != completionEpoch {
314+ return true
315+ }
316+ return !currentRegistration.Equal(completionRegistration)
317+}
318+
319+// BrokerOpenStreamRequest asks the broker to open one independently
320+// cancellable logical stream to the owning daemon. The daemon revalidates
321+// the exact session identity before attachment. Env is the per-request
322+// session environment; it is never inherited from the broker process
323+// environment.
324+type BrokerOpenStreamRequest struct {
325+ Connection BrokerConnectionID
326+ Stream BrokerStreamID
327+ Endpoint string
328+ Registration domain.RemoteRegistration
329+ Target protocol.ExactSessionTarget
330+ Env []string
331+ Policy BrokerPolicy
332+}
333+
334+func (r BrokerOpenStreamRequest) Validate() error {
335+ if err := r.Connection.Validate(); err != nil {
336+ return err
337+ }
338+ if err := r.Stream.Validate(); err != nil {
339+ return err
340+ }
341+ if err := domain.ValidateRemoteHostTarget(r.Endpoint); err != nil {
342+ return fmt.Errorf("ports: broker open stream: %w", err)
343+ }
344+ if err := r.Registration.Validate(); err != nil {
345+ return fmt.Errorf("ports: broker open stream: %w", err)
346+ }
347+ if r.Endpoint != r.Registration.Endpoint {
348+ return errors.New("ports: broker open stream endpoint does not match registration")
349+ }
350+ if err := r.Target.Validate(); err != nil {
351+ return fmt.Errorf("ports: broker open stream: %w", err)
352+ }
353+ if uint64(len(r.Env)) > BrokerMaxEnvEntries {
354+ return errors.New("ports: broker open stream has too many environment entries")
355+ }
356+ for _, entry := range r.Env {
357+ if len(entry) > BrokerMaxEnvEntryBytes {
358+ return errors.New("ports: broker open stream environment entry too long")
359+ }
360+ if !utf8.ValidString(entry) {
361+ return errors.New("ports: broker open stream environment is not valid UTF-8")
362+ }
363+ }
364+ if err := r.Policy.Validate(); err != nil {
365+ return err
366+ }
367+ return nil
368+}
369+
370+// BrokerMutationOutcome is the closed outcome taxonomy for a mutating
371+// broker operation. OutcomeUnknown means connectivity was lost after the
372+// mutation may have committed but before its result arrived: the client
373+// refreshes authoritative state and never blindly replays a
374+// non-idempotent operation.
375+type BrokerMutationOutcome uint8
376+
377+const (
378+ BrokerOutcomeOK BrokerMutationOutcome = iota + 1
379+ BrokerOutcomeFailed
380+ BrokerOutcomeUnknown
381+)
382+
383+func (o BrokerMutationOutcome) String() string {
384+ switch o {
385+ case BrokerOutcomeOK:
386+ return "ok"
387+ case BrokerOutcomeFailed:
388+ return "failed"
389+ case BrokerOutcomeUnknown:
390+ return "outcome_unknown"
391+ default:
392+ return "unknown"
393+ }
394+}
395+
396+func (o BrokerMutationOutcome) Validate() error {
397+ switch o {
398+ case BrokerOutcomeOK, BrokerOutcomeFailed, BrokerOutcomeUnknown:
399+ return nil
400+ default:
401+ return errors.New("ports: invalid broker mutation outcome")
402+ }
403+}
404+
405+// BrokerOperationResult is exactly one completion for one admitted mutating
406+// operation. Text is bounded and presentation-safe; the outcome decides
407+// whether the client may retry, must refresh, or must not replay.
408+type BrokerOperationResult struct {
409+ Operation BrokerOperationID
410+ Outcome BrokerMutationOutcome
411+ Text string
412+}
413+
414+func (r BrokerOperationResult) Validate() error {
415+ if err := r.Operation.Validate(); err != nil {
416+ return err
417+ }
418+ if err := r.Outcome.Validate(); err != nil {
419+ return err
420+ }
421+ if err := validateBrokerDisplayText(r.Text, BrokerMaxErrorBytes, "operation result text"); err != nil {
422+ return err
423+ }
424+ if r.Outcome == BrokerOutcomeOK && r.Text != "" {
425+ return errors.New("ports: successful broker operation carries no text")
426+ }
427+ return nil
428+}
429+
430+// BrokerErrorCode is the closed taxonomy for broker and stream lifecycle
431+// failures surfaced to clients. Every failure returns the client to the
432+// picker in the same process with a visible typed error; none terminates
433+// the client except an explicit exit or a fatal terminal failure.
434+type BrokerErrorCode uint8
435+
436+const (
437+ BrokerErrorUnavailable BrokerErrorCode = iota + 1
438+ BrokerErrorIncompatible
439+ BrokerErrorTimeout
440+ BrokerErrorCancelled
441+ BrokerErrorOutcomeUnknown
442+ BrokerErrorAttachmentLost
443+ BrokerErrorStaleEpoch
444+ BrokerErrorConflictingPolicy
445+ BrokerErrorExplicitExit
446+ BrokerErrorFatalTerminal
447+)
448+
449+func (c BrokerErrorCode) String() string {
450+ switch c {
451+ case BrokerErrorUnavailable:
452+ return "unavailable"
453+ case BrokerErrorIncompatible:
454+ return "incompatible"
455+ case BrokerErrorTimeout:
456+ return "timeout"
457+ case BrokerErrorCancelled:
458+ return "cancelled"
459+ case BrokerErrorOutcomeUnknown:
460+ return "outcome_unknown"
461+ case BrokerErrorAttachmentLost:
462+ return "attachment_lost"
463+ case BrokerErrorStaleEpoch:
464+ return "stale_epoch"
465+ case BrokerErrorConflictingPolicy:
466+ return "conflicting_policy"
467+ case BrokerErrorExplicitExit:
468+ return "explicit_exit"
469+ case BrokerErrorFatalTerminal:
470+ return "fatal_terminal"
471+ default:
472+ return "unknown"
473+ }
474+}
475+
476+func (c BrokerErrorCode) Validate() error {
477+ switch c {
478+ case BrokerErrorUnavailable, BrokerErrorIncompatible, BrokerErrorTimeout,
479+ BrokerErrorCancelled, BrokerErrorOutcomeUnknown, BrokerErrorAttachmentLost,
480+ BrokerErrorStaleEpoch, BrokerErrorConflictingPolicy,
481+ BrokerErrorExplicitExit, BrokerErrorFatalTerminal:
482+ return nil
483+ default:
484+ return errors.New("ports: invalid broker error code")
485+ }
486+}
487+
488+// BrokerError is the typed lifecycle failure carried to clients. Terminal
489+// reports whether the client process itself must exit: only an explicit
490+// exit or a fatal terminal failure is terminal; every other code returns
491+// to the picker with a visible notification.
492+type BrokerError struct {
493+ Code BrokerErrorCode
494+ Text string
495+}
496+
497+func (e BrokerError) Error() string {
498+ if e.Text == "" {
499+ return "vev: broker " + e.Code.String()
500+ }
501+ return "vev: broker " + e.Code.String() + ": " + e.Text
502+}
503+
504+func (e BrokerError) Validate() error {
505+ if err := e.Code.Validate(); err != nil {
506+ return err
507+ }
508+ return validateBrokerDisplayText(e.Text, BrokerMaxErrorBytes, "error text")
509+}
510+
511+// Terminal reports whether this failure ends the client process.
512+func (e BrokerError) Terminal() bool {
513+ return e.Code == BrokerErrorExplicitExit || e.Code == BrokerErrorFatalTerminal
514+}
515+
516+// BrokerStreamLost is the independent typed loss event delivered per
517+// affected logical stream when its physical connection fails. One stream
518+// failure never closes unrelated streams; a physical loss fans out one
519+// event per stream.
520+type BrokerStreamLost struct {
521+ Connection BrokerConnectionID
522+ Stream BrokerStreamID
523+ Epoch BrokerEpoch
524+ Cause domain.RemoteFailureKind
525+}
526+
527+func (e BrokerStreamLost) Validate() error {
528+ if err := e.Connection.Validate(); err != nil {
529+ return err
530+ }
531+ if err := e.Stream.Validate(); err != nil {
532+ return err
533+ }
534+ if e.Epoch == 0 {
535+ return errors.New("ports: broker stream loss has no epoch")
536+ }
537+ return nil
538+}
539+
540+// BrokerSubscription wakes one subscriber when a newer snapshot is
541+// available. Each subscriber owns a capacity-one channel and re-reads the
542+// newest snapshot on wake; channels are never shared between subscribers.
543+type BrokerSubscription interface {
544+ Changed() <-chan struct{}
545+ Close()
546+}
547+
548+// BrokerService is the client-facing broker façade. All clients reach
549+// local and remote daemons through it, including the local daemon.
550+// Snapshot access performs no I/O; request methods are non-blocking
551+// coalesced hints except where the context explicitly bounds them.
552+type BrokerService interface {
553+ // ConnectionID returns the ID assigned when this client connection was
554+ // accepted. Clients carry it on stream operations to fence stale requests.
555+ ConnectionID() BrokerConnectionID
556+ Snapshot() BrokerSnapshot
557+ Subscribe() (BrokerSubscription, error)
558+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error)
559+ CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error
560+ AddHost(ctx context.Context, target string) error
561+ RemoveHost(ctx context.Context, target string) (bool, error)
562+ RequestReconcile(endpoint string)
563+ Close() error
564+}
565+
566+// BrokerSnapshotStore persists the durable broker snapshot independently
567+// from session state. The broker is its sole writer; session persistence
568+// stays daemon-owned and is never moved here.
569+type BrokerSnapshotStore interface {
570+ Load() (BrokerSnapshot, error)
571+ Store(BrokerSnapshot) error
572+}
573+
574+// BrokerPhysicalConnection is one pooled physical transport to an
575+// authenticated daemon identity. It opens one independently cancellable
576+// typed session connection per logical stream and never shares stream
577+// state, identity, geometry, input, output, cancellation, or application
578+// errors between streams.
579+type BrokerPhysicalConnection interface {
580+ Identity() BrokerDaemonIdentity
581+ Incarnation() BrokerDaemonIncarnation
582+ Policy() BrokerPolicy
583+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error)
584+ Close() error
585+}
586+
587+// BrokerEndpointConnector establishes the pooled physical transport for
588+// one endpoint under an exact compatible policy. Conflicting policy is
589+// rejected; the first launching client never becomes policy authority.
590+type BrokerEndpointConnector interface {
591+ Connect(ctx context.Context, endpoint string, policy BrokerPolicy) (BrokerPhysicalConnection, error)
592+}
593+
594+// BrokerListener accepts client connections to the per-user broker
595+// endpoint. Each Accept returns a BrokerService bound to exactly one
596+// accepted client connection: the implementation assigns that
597+// connection's BrokerConnectionID at accept, ends the connection on
598+// Close, and rejects requests carrying a mismatched Connection as stale.
599+// Implementations own the owner-only directory/socket, peer credential
600+// checks, bounded clients and queues, and cancellation-safe accept loops.
601+type BrokerListener interface {
602+ Accept() (BrokerService, error)
603+ Close() error
604+ Addr() string
605+}
606+
607+func validateBrokerToken(value string, maxBytes int, label string) error {
608+ if value == "" {
609+ return fmt.Errorf("ports: broker %s is empty", label)
610+ }
611+ if len(value) > maxBytes {
612+ return fmt.Errorf("ports: broker %s too long", label)
613+ }
614+ if !utf8.ValidString(value) {
615+ return fmt.Errorf("ports: broker %s is not valid UTF-8", label)
616+ }
617+ if strings.TrimSpace(value) != value {
618+ return fmt.Errorf("ports: broker %s has surrounding whitespace", label)
619+ }
620+ for _, r := range value {
621+ if unicode.IsControl(r) || unicode.IsSpace(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
622+ return fmt.Errorf("ports: broker %s contains disallowed characters", label)
623+ }
624+ }
625+ return nil
626+}
627+
628+func validateBrokerDisplayText(value string, maxBytes int, label string) error {
629+ if len(value) > maxBytes {
630+ return fmt.Errorf("ports: broker %s too long", label)
631+ }
632+ if !utf8.ValidString(value) {
633+ return fmt.Errorf("ports: broker %s is not valid UTF-8", label)
634+ }
635+ for _, r := range value {
636+ if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
637+ return fmt.Errorf("ports: broker %s contains disallowed characters", label)
638+ }
639+ }
640+ return nil
641+}
@@ -0,0 +1,641 @@
1+package ports
2+
3+import (
4+ "context"
5+ "errors"
6+ "fmt"
7+ "strings"
8+ "unicode"
9+ "unicode/utf8"
10+
11+ "github.com/bnema/vev/internal/domain"
12+ "github.com/bnema/vev/internal/protocol"
13+ "github.com/bnema/vev/internal/protocol/catalogue"
14+)
15+
16+// Broker semantic contracts (Plan 001 P1.3).
17+//
18+// The broker is the machine-wide owner of connectivity: configured hosts,
19+// daemon reachability and catalogue snapshots, pooled physical transports,
20+// and multiplexed logical streams. It never owns picker UI, navigation
21+// decisions, sessions, PTYs, terminal geometry, or attachment semantics.
22+//
23+// Identity levels stay distinct:
24+// - configured host registration/incarnation (domain.RemoteRegistration)
25+// - authenticated daemon identity used for pooling (BrokerDaemonIdentity)
26+// - daemon process incarnation used to detect restart
27+// (BrokerDaemonIncarnation)
28+// - exact session lifecycle identity (domain.SessionLifecycleID via
29+// protocol.ExactSessionTarget)
30+// - broker epoch/revision (BrokerEpoch/BrokerRevision)
31+// - client connection identity (BrokerConnectionID)
32+// - logical stream identity (BrokerStreamID)
33+//
34+// An epoch change forces a full resynchronization: stream IDs and events
35+// are scoped to one client connection and broker epoch, so stale
36+// completions cannot revive an attachment. Tombstones and registration
37+// incarnation fence remove/re-add races independently of the epoch.
38+//
39+// Raw frames, codecs, environment policy enforcement, and worker
40+// implementations live outside this package. Per-request session
41+// environment (BrokerOpenStreamRequest.Env) stays separate from the broker
42+// process environment.
43+
44+const (
45+ // BrokerMaxHosts bounds one snapshot publication. It mirrors the
46+ // catalogue host budget so a broker snapshot never exceeds what
47+ // discovery can produce.
48+ BrokerMaxHosts = catalogue.RemoteCatalogMaxHosts
49+ // BrokerMaxSessionsPerHost bounds sessions carried per host in one
50+ // snapshot publication.
51+ BrokerMaxSessionsPerHost = catalogue.RemoteCatalogMaxSessions
52+ // BrokerMaxTombstones bounds retired registrations carried alongside
53+ // one snapshot for remove/re-add fencing.
54+ BrokerMaxTombstones = catalogue.RemoteCatalogMaxHosts
55+ // BrokerMaxErrorBytes bounds human-readable error text in broker
56+ // results. The underlying cause stays behind errors.Is/As where the
57+ // contract carries an error value.
58+ BrokerMaxErrorBytes = 512
59+ // BrokerMaxEnvEntries bounds per-request environment entries. The
60+ // broker never merges these into its own process environment.
61+ BrokerMaxEnvEntries = 128
62+ // BrokerMaxEnvEntryBytes bounds one per-request environment entry.
63+ BrokerMaxEnvEntryBytes = 4 << 10
64+ // BrokerMaxTransportBytes bounds the opaque transport policy token.
65+ BrokerMaxTransportBytes = 64
66+ // BrokerMaxIdentityBytes bounds the authenticated daemon identity token.
67+ BrokerMaxIdentityBytes = 256
68+)
69+
70+// BrokerEpoch identifies one broker process incarnation. Every broker
71+// process samples a fresh non-zero epoch at startup; every publication
72+// carries a revision within that epoch. Clients discard all broker state
73+// and resubscribe when the epoch changes.
74+type BrokerEpoch uint64
75+
76+// BrokerRevision is monotonic within one broker epoch. Revisions never
77+// compare across epochs.
78+type BrokerRevision uint64
79+
80+// BrokerConnectionID identifies one client connection to the broker,
81+// scoped to a broker epoch. It never identifies a session or a stream.
82+type BrokerConnectionID [16]byte
83+
84+// BrokerStreamID identifies one logical stream within a client connection
85+// and broker epoch. It never crosses connections or epochs.
86+type BrokerStreamID uint64
87+
88+// BrokerOperationID is the idempotency identity for one mutating broker
89+// operation. Clients refresh authoritative state on outcome-unknown and
90+// never blindly replay a non-idempotent operation.
91+type BrokerOperationID [16]byte
92+
93+// BrokerDaemonIncarnation identifies one daemon process incarnation and
94+// detects restarts behind a stable authenticated identity.
95+type BrokerDaemonIncarnation [16]byte
96+
97+func (id BrokerConnectionID) IsZero() bool { return id == (BrokerConnectionID{}) }
98+
99+func (id BrokerConnectionID) Validate() error {
100+ if id.IsZero() {
101+ return errors.New("ports: broker connection ID is zero")
102+ }
103+ return nil
104+}
105+
106+func (id BrokerStreamID) Validate() error {
107+ if id == 0 {
108+ return errors.New("ports: broker stream ID is zero")
109+ }
110+ return nil
111+}
112+
113+func (id BrokerOperationID) IsZero() bool { return id == (BrokerOperationID{}) }
114+
115+func (id BrokerOperationID) Validate() error {
116+ if id.IsZero() {
117+ return errors.New("ports: broker operation ID is zero")
118+ }
119+ return nil
120+}
121+
122+func (id BrokerDaemonIncarnation) IsZero() bool { return id == (BrokerDaemonIncarnation{}) }
123+
124+func (id BrokerDaemonIncarnation) Validate() error {
125+ if id.IsZero() {
126+ return errors.New("ports: broker daemon incarnation is zero")
127+ }
128+ return nil
129+}
130+
131+// BrokerDaemonIdentity is the authenticated daemon/service identity used
132+// for physical transport pooling. Display aliases are never pooled: two
133+// endpoints share a transport only after authenticated identity and every
134+// trust, launch, isolation, and transport policy compare compatible.
135+type BrokerDaemonIdentity string
136+
137+func (id BrokerDaemonIdentity) Validate() error {
138+ return validateBrokerToken(string(id), BrokerMaxIdentityBytes, "daemon identity")
139+}
140+
141+// BrokerPolicy is the exact connection policy required before two
142+// endpoints may share a pooled physical transport. Pooling requires exact
143+// equality: conflicting policy is rejected, never merged or inherited
144+// from the first launching client.
145+type BrokerPolicy struct {
146+ ProtocolVersion uint16
147+ CatalogSchemaVersion uint16
148+ EnvironmentPolicy protocol.EnvironmentPolicy
149+ Transport string
150+}
151+
152+func (p BrokerPolicy) Validate() error {
153+ if p.ProtocolVersion == 0 {
154+ return errors.New("ports: broker policy has no protocol version")
155+ }
156+ if p.CatalogSchemaVersion == 0 {
157+ return errors.New("ports: broker policy has no catalog schema version")
158+ }
159+ if err := protocol.ValidateEnvironmentPolicy(p.EnvironmentPolicy); err != nil {
160+ return fmt.Errorf("ports: broker policy: %w", err)
161+ }
162+ if err := validateBrokerToken(p.Transport, BrokerMaxTransportBytes, "policy transport"); err != nil {
163+ return err
164+ }
165+ return nil
166+}
167+
168+// Compatible reports whether two validated policies may share a pooled
169+// physical transport. It requires exact equality across every field.
170+func (p BrokerPolicy) Compatible(other BrokerPolicy) bool {
171+ return p.ProtocolVersion == other.ProtocolVersion &&
172+ p.CatalogSchemaVersion == other.CatalogSchemaVersion &&
173+ p.EnvironmentPolicy == other.EnvironmentPolicy &&
174+ p.Transport == other.Transport
175+}
176+
177+// BrokerHostTombstone fences a removed host registration so a stale probe,
178+// snapshot, or completion bound to the retired incarnation cannot overwrite
179+// or revive current authority. A re-added endpoint carries a new
180+// incarnation and is never fenced by an older tombstone.
181+type BrokerHostTombstone struct {
182+ Endpoint string
183+ Registration domain.RemoteRegistration
184+ RetiredRevision BrokerRevision
185+}
186+
187+func (t BrokerHostTombstone) Validate() error {
188+ if err := domain.ValidateRemoteHostTarget(t.Endpoint); err != nil {
189+ return fmt.Errorf("ports: broker tombstone: %w", err)
190+ }
191+ if err := t.Registration.Validate(); err != nil {
192+ return fmt.Errorf("ports: broker tombstone: %w", err)
193+ }
194+ if t.Endpoint != t.Registration.Endpoint {
195+ return errors.New("ports: broker tombstone endpoint does not match registration")
196+ }
197+ if t.RetiredRevision == 0 {
198+ return errors.New("ports: broker tombstone has no retired revision")
199+ }
200+ return nil
201+}
202+
203+// Fences reports whether this tombstone retires the supplied registration:
204+// same endpoint and incarnation, with a retired generation at or past the
205+// candidate. A re-added endpoint with a fresh incarnation is never fenced.
206+func (t BrokerHostTombstone) Fences(candidate domain.RemoteRegistration) bool {
207+ return t.Endpoint == candidate.Endpoint &&
208+ t.Registration.Incarnation == candidate.Incarnation &&
209+ t.Registration.Generation >= candidate.Generation
210+}
211+
212+// BrokerSnapshot is an immutable, fully defensive broker publication. Hosts
213+// reuse the ordered RemoteHostSnapshot projection; Removed carries the
214+// bounded tombstone set that fences stale authority. Nested slices are
215+// never mutated after publication.
216+type BrokerSnapshot struct {
217+ Epoch BrokerEpoch
218+ Revision BrokerRevision
219+ Hosts []RemoteHostSnapshot
220+ Removed []BrokerHostTombstone
221+}
222+
223+func (s BrokerSnapshot) Validate() error {
224+ if s.Epoch == 0 {
225+ return errors.New("ports: broker snapshot has no epoch")
226+ }
227+ if s.Revision == 0 {
228+ return errors.New("ports: broker snapshot has no revision")
229+ }
230+ if len(s.Hosts) > BrokerMaxHosts {
231+ return errors.New("ports: broker snapshot has too many hosts")
232+ }
233+ if len(s.Removed) > BrokerMaxTombstones {
234+ return errors.New("ports: broker snapshot has too many tombstones")
235+ }
236+ seen := make(map[string]struct{}, len(s.Hosts))
237+ for _, host := range s.Hosts {
238+ if err := domain.ValidateRemoteHostTarget(host.Endpoint); err != nil {
239+ return fmt.Errorf("ports: broker snapshot: %w", err)
240+ }
241+ if err := host.Registration.Validate(); err != nil {
242+ return fmt.Errorf("ports: broker snapshot: %w", err)
243+ }
244+ if host.Endpoint != host.Registration.Endpoint {
245+ return errors.New("ports: broker snapshot host endpoint does not match registration")
246+ }
247+ if len(host.Sessions) > BrokerMaxSessionsPerHost {
248+ return errors.New("ports: broker snapshot host has too many sessions")
249+ }
250+ if _, dup := seen[host.Endpoint]; dup {
251+ return fmt.Errorf("ports: broker snapshot has duplicate host %q", host.Endpoint)
252+ }
253+ seen[host.Endpoint] = struct{}{}
254+ }
255+ for _, tombstone := range s.Removed {
256+ if err := tombstone.Validate(); err != nil {
257+ return err
258+ }
259+ if _, live := seen[tombstone.Endpoint]; live {
260+ return fmt.Errorf("ports: broker snapshot carries live host %q as tombstone", tombstone.Endpoint)
261+ }
262+ }
263+ return nil
264+}
265+
266+// Clone returns a defensive copy with independent host, session, tab, and
267+// tombstone slices.
268+func (s BrokerSnapshot) Clone() BrokerSnapshot {
269+ out := s
270+ out.Hosts = make([]RemoteHostSnapshot, len(s.Hosts))
271+ for i, host := range s.Hosts {
272+ out.Hosts[i] = host.Clone()
273+ }
274+ out.Removed = append([]BrokerHostTombstone(nil), s.Removed...)
275+ return out
276+}
277+
278+// Find returns the snapshot for an exact endpoint identity.
279+func (s BrokerSnapshot) Find(endpoint string) (RemoteHostSnapshot, bool) {
280+ for _, host := range s.Hosts {
281+ if host.Endpoint == endpoint {
282+ return host, true
283+ }
284+ }
285+ return RemoteHostSnapshot{}, false
286+}
287+
288+// Supersedes reports whether s is newer than other. Any epoch change
289+// forces a full resynchronization: a snapshot with a valid epoch
290+// supersedes every snapshot from another epoch (or none), regardless of
291+// numeric order, because epochs are fresh per broker process and never
292+// ordered across processes. Within one epoch the higher revision wins.
293+// Revisions never compare across epochs. A zero-epoch snapshot is
294+// invalid and never supersedes.
295+func (s BrokerSnapshot) Supersedes(other BrokerSnapshot) bool {
296+ if s.Epoch == 0 {
297+ return false
298+ }
299+ if s.Epoch != other.Epoch {
300+ return true
301+ }
302+ return s.Revision > other.Revision
303+}
304+
305+// BrokerCompletionIsStale reports whether a probe or operation completion
306+// must be rejected: a different broker epoch, or a registration that no
307+// longer exactly matches current authority (endpoint, incarnation, and
308+// generation must all match).
309+func BrokerCompletionIsStale(currentRegistration domain.RemoteRegistration, currentEpoch BrokerEpoch, completionRegistration domain.RemoteRegistration, completionEpoch BrokerEpoch) bool {
310+ if currentEpoch == 0 || completionEpoch == 0 {
311+ return true
312+ }
313+ if currentEpoch != completionEpoch {
314+ return true
315+ }
316+ return !currentRegistration.Equal(completionRegistration)
317+}
318+
319+// BrokerOpenStreamRequest asks the broker to open one independently
320+// cancellable logical stream to the owning daemon. The daemon revalidates
321+// the exact session identity before attachment. Env is the per-request
322+// session environment; it is never inherited from the broker process
323+// environment.
324+type BrokerOpenStreamRequest struct {
325+ Connection BrokerConnectionID
326+ Stream BrokerStreamID
327+ Endpoint string
328+ Registration domain.RemoteRegistration
329+ Target protocol.ExactSessionTarget
330+ Env []string
331+ Policy BrokerPolicy
332+}
333+
334+func (r BrokerOpenStreamRequest) Validate() error {
335+ if err := r.Connection.Validate(); err != nil {
336+ return err
337+ }
338+ if err := r.Stream.Validate(); err != nil {
339+ return err
340+ }
341+ if err := domain.ValidateRemoteHostTarget(r.Endpoint); err != nil {
342+ return fmt.Errorf("ports: broker open stream: %w", err)
343+ }
344+ if err := r.Registration.Validate(); err != nil {
345+ return fmt.Errorf("ports: broker open stream: %w", err)
346+ }
347+ if r.Endpoint != r.Registration.Endpoint {
348+ return errors.New("ports: broker open stream endpoint does not match registration")
349+ }
350+ if err := r.Target.Validate(); err != nil {
351+ return fmt.Errorf("ports: broker open stream: %w", err)
352+ }
353+ if uint64(len(r.Env)) > BrokerMaxEnvEntries {
354+ return errors.New("ports: broker open stream has too many environment entries")
355+ }
356+ for _, entry := range r.Env {
357+ if len(entry) > BrokerMaxEnvEntryBytes {
358+ return errors.New("ports: broker open stream environment entry too long")
359+ }
360+ if !utf8.ValidString(entry) {
361+ return errors.New("ports: broker open stream environment is not valid UTF-8")
362+ }
363+ }
364+ if err := r.Policy.Validate(); err != nil {
365+ return err
366+ }
367+ return nil
368+}
369+
370+// BrokerMutationOutcome is the closed outcome taxonomy for a mutating
371+// broker operation. OutcomeUnknown means connectivity was lost after the
372+// mutation may have committed but before its result arrived: the client
373+// refreshes authoritative state and never blindly replays a
374+// non-idempotent operation.
375+type BrokerMutationOutcome uint8
376+
377+const (
378+ BrokerOutcomeOK BrokerMutationOutcome = iota + 1
379+ BrokerOutcomeFailed
380+ BrokerOutcomeUnknown
381+)
382+
383+func (o BrokerMutationOutcome) String() string {
384+ switch o {
385+ case BrokerOutcomeOK:
386+ return "ok"
387+ case BrokerOutcomeFailed:
388+ return "failed"
389+ case BrokerOutcomeUnknown:
390+ return "outcome_unknown"
391+ default:
392+ return "unknown"
393+ }
394+}
395+
396+func (o BrokerMutationOutcome) Validate() error {
397+ switch o {
398+ case BrokerOutcomeOK, BrokerOutcomeFailed, BrokerOutcomeUnknown:
399+ return nil
400+ default:
401+ return errors.New("ports: invalid broker mutation outcome")
402+ }
403+}
404+
405+// BrokerOperationResult is exactly one completion for one admitted mutating
406+// operation. Text is bounded and presentation-safe; the outcome decides
407+// whether the client may retry, must refresh, or must not replay.
408+type BrokerOperationResult struct {
409+ Operation BrokerOperationID
410+ Outcome BrokerMutationOutcome
411+ Text string
412+}
413+
414+func (r BrokerOperationResult) Validate() error {
415+ if err := r.Operation.Validate(); err != nil {
416+ return err
417+ }
418+ if err := r.Outcome.Validate(); err != nil {
419+ return err
420+ }
421+ if err := validateBrokerDisplayText(r.Text, BrokerMaxErrorBytes, "operation result text"); err != nil {
422+ return err
423+ }
424+ if r.Outcome == BrokerOutcomeOK && r.Text != "" {
425+ return errors.New("ports: successful broker operation carries no text")
426+ }
427+ return nil
428+}
429+
430+// BrokerErrorCode is the closed taxonomy for broker and stream lifecycle
431+// failures surfaced to clients. Every failure returns the client to the
432+// picker in the same process with a visible typed error; none terminates
433+// the client except an explicit exit or a fatal terminal failure.
434+type BrokerErrorCode uint8
435+
436+const (
437+ BrokerErrorUnavailable BrokerErrorCode = iota + 1
438+ BrokerErrorIncompatible
439+ BrokerErrorTimeout
440+ BrokerErrorCancelled
441+ BrokerErrorOutcomeUnknown
442+ BrokerErrorAttachmentLost
443+ BrokerErrorStaleEpoch
444+ BrokerErrorConflictingPolicy
445+ BrokerErrorExplicitExit
446+ BrokerErrorFatalTerminal
447+)
448+
449+func (c BrokerErrorCode) String() string {
450+ switch c {
451+ case BrokerErrorUnavailable:
452+ return "unavailable"
453+ case BrokerErrorIncompatible:
454+ return "incompatible"
455+ case BrokerErrorTimeout:
456+ return "timeout"
457+ case BrokerErrorCancelled:
458+ return "cancelled"
459+ case BrokerErrorOutcomeUnknown:
460+ return "outcome_unknown"
461+ case BrokerErrorAttachmentLost:
462+ return "attachment_lost"
463+ case BrokerErrorStaleEpoch:
464+ return "stale_epoch"
465+ case BrokerErrorConflictingPolicy:
466+ return "conflicting_policy"
467+ case BrokerErrorExplicitExit:
468+ return "explicit_exit"
469+ case BrokerErrorFatalTerminal:
470+ return "fatal_terminal"
471+ default:
472+ return "unknown"
473+ }
474+}
475+
476+func (c BrokerErrorCode) Validate() error {
477+ switch c {
478+ case BrokerErrorUnavailable, BrokerErrorIncompatible, BrokerErrorTimeout,
479+ BrokerErrorCancelled, BrokerErrorOutcomeUnknown, BrokerErrorAttachmentLost,
480+ BrokerErrorStaleEpoch, BrokerErrorConflictingPolicy,
481+ BrokerErrorExplicitExit, BrokerErrorFatalTerminal:
482+ return nil
483+ default:
484+ return errors.New("ports: invalid broker error code")
485+ }
486+}
487+
488+// BrokerError is the typed lifecycle failure carried to clients. Terminal
489+// reports whether the client process itself must exit: only an explicit
490+// exit or a fatal terminal failure is terminal; every other code returns
491+// to the picker with a visible notification.
492+type BrokerError struct {
493+ Code BrokerErrorCode
494+ Text string
495+}
496+
497+func (e BrokerError) Error() string {
498+ if e.Text == "" {
499+ return "vev: broker " + e.Code.String()
500+ }
501+ return "vev: broker " + e.Code.String() + ": " + e.Text
502+}
503+
504+func (e BrokerError) Validate() error {
505+ if err := e.Code.Validate(); err != nil {
506+ return err
507+ }
508+ return validateBrokerDisplayText(e.Text, BrokerMaxErrorBytes, "error text")
509+}
510+
511+// Terminal reports whether this failure ends the client process.
512+func (e BrokerError) Terminal() bool {
513+ return e.Code == BrokerErrorExplicitExit || e.Code == BrokerErrorFatalTerminal
514+}
515+
516+// BrokerStreamLost is the independent typed loss event delivered per
517+// affected logical stream when its physical connection fails. One stream
518+// failure never closes unrelated streams; a physical loss fans out one
519+// event per stream.
520+type BrokerStreamLost struct {
521+ Connection BrokerConnectionID
522+ Stream BrokerStreamID
523+ Epoch BrokerEpoch
524+ Cause domain.RemoteFailureKind
525+}
526+
527+func (e BrokerStreamLost) Validate() error {
528+ if err := e.Connection.Validate(); err != nil {
529+ return err
530+ }
531+ if err := e.Stream.Validate(); err != nil {
532+ return err
533+ }
534+ if e.Epoch == 0 {
535+ return errors.New("ports: broker stream loss has no epoch")
536+ }
537+ return nil
538+}
539+
540+// BrokerSubscription wakes one subscriber when a newer snapshot is
541+// available. Each subscriber owns a capacity-one channel and re-reads the
542+// newest snapshot on wake; channels are never shared between subscribers.
543+type BrokerSubscription interface {
544+ Changed() <-chan struct{}
545+ Close()
546+}
547+
548+// BrokerService is the client-facing broker façade. All clients reach
549+// local and remote daemons through it, including the local daemon.
550+// Snapshot access performs no I/O; request methods are non-blocking
551+// coalesced hints except where the context explicitly bounds them.
552+type BrokerService interface {
553+ // ConnectionID returns the ID assigned when this client connection was
554+ // accepted. Clients carry it on stream operations to fence stale requests.
555+ ConnectionID() BrokerConnectionID
556+ Snapshot() BrokerSnapshot
557+ Subscribe() (BrokerSubscription, error)
558+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error)
559+ CloseStream(connection BrokerConnectionID, stream BrokerStreamID) error
560+ AddHost(ctx context.Context, target string) error
561+ RemoveHost(ctx context.Context, target string) (bool, error)
562+ RequestReconcile(endpoint string)
563+ Close() error
564+}
565+
566+// BrokerSnapshotStore persists the durable broker snapshot independently
567+// from session state. The broker is its sole writer; session persistence
568+// stays daemon-owned and is never moved here.
569+type BrokerSnapshotStore interface {
570+ Load() (BrokerSnapshot, error)
571+ Store(BrokerSnapshot) error
572+}
573+
574+// BrokerPhysicalConnection is one pooled physical transport to an
575+// authenticated daemon identity. It opens one independently cancellable
576+// typed session connection per logical stream and never shares stream
577+// state, identity, geometry, input, output, cancellation, or application
578+// errors between streams.
579+type BrokerPhysicalConnection interface {
580+ Identity() BrokerDaemonIdentity
581+ Incarnation() BrokerDaemonIncarnation
582+ Policy() BrokerPolicy
583+ OpenStream(ctx context.Context, request BrokerOpenStreamRequest) (ClientConnection, error)
584+ Close() error
585+}
586+
587+// BrokerEndpointConnector establishes the pooled physical transport for
588+// one endpoint under an exact compatible policy. Conflicting policy is
589+// rejected; the first launching client never becomes policy authority.
590+type BrokerEndpointConnector interface {
591+ Connect(ctx context.Context, endpoint string, policy BrokerPolicy) (BrokerPhysicalConnection, error)
592+}
593+
594+// BrokerListener accepts client connections to the per-user broker
595+// endpoint. Each Accept returns a BrokerService bound to exactly one
596+// accepted client connection: the implementation assigns that
597+// connection's BrokerConnectionID at accept, ends the connection on
598+// Close, and rejects requests carrying a mismatched Connection as stale.
599+// Implementations own the owner-only directory/socket, peer credential
600+// checks, bounded clients and queues, and cancellation-safe accept loops.
601+type BrokerListener interface {
602+ Accept() (BrokerService, error)
603+ Close() error
604+ Addr() string
605+}
606+
607+func validateBrokerToken(value string, maxBytes int, label string) error {
608+ if value == "" {
609+ return fmt.Errorf("ports: broker %s is empty", label)
610+ }
611+ if len(value) > maxBytes {
612+ return fmt.Errorf("ports: broker %s too long", label)
613+ }
614+ if !utf8.ValidString(value) {
615+ return fmt.Errorf("ports: broker %s is not valid UTF-8", label)
616+ }
617+ if strings.TrimSpace(value) != value {
618+ return fmt.Errorf("ports: broker %s has surrounding whitespace", label)
619+ }
620+ for _, r := range value {
621+ if unicode.IsControl(r) || unicode.IsSpace(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
622+ return fmt.Errorf("ports: broker %s contains disallowed characters", label)
623+ }
624+ }
625+ return nil
626+}
627+
628+func validateBrokerDisplayText(value string, maxBytes int, label string) error {
629+ if len(value) > maxBytes {
630+ return fmt.Errorf("ports: broker %s too long", label)
631+ }
632+ if !utf8.ValidString(value) {
633+ return fmt.Errorf("ports: broker %s is not valid UTF-8", label)
634+ }
635+ for _, r := range value {
636+ if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' {
637+ return fmt.Errorf("ports: broker %s contains disallowed characters", label)
638+ }
639+ }
640+ return nil
641+}
A internal/ports/broker_test.go +479 -0
@@ -0,0 +1,479 @@
1+package ports
2+
3+import (
4+ "reflect"
5+ "strings"
6+ "testing"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
10+ "github.com/bnema/vev/internal/protocol/catalogue"
11+)
12+
13+func testBrokerRegistration(endpoint string, incarnation byte, generation domain.RemoteGeneration) domain.RemoteRegistration {
14+ return domain.RemoteRegistration{
15+ Endpoint: endpoint,
16+ Incarnation: [16]byte{incarnation, 7, 9},
17+ Generation: generation,
18+ }
19+}
20+
21+func testBrokerLifecycle() domain.SessionLifecycleID {
22+ return domain.SessionLifecycleID{1, 2, 3}
23+}
24+
25+func testBrokerTarget() protocol.ExactSessionTarget {
26+ return protocol.ExactSessionTarget{LifecycleID: testBrokerLifecycle(), SessionName: "work"}
27+}
28+
29+func testBrokerPolicy() BrokerPolicy {
30+ return BrokerPolicy{
31+ ProtocolVersion: protocol.Version,
32+ CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion,
33+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
34+ Transport: "quic",
35+ }
36+}
37+
38+func testBrokerHost(endpoint string) RemoteHostSnapshot {
39+ return RemoteHostSnapshot{
40+ Endpoint: endpoint,
41+ DisplayOrigin: endpoint,
42+ Registration: testBrokerRegistration(endpoint, 1, 1),
43+ Availability: domain.RemoteAvailabilityReachable,
44+ }
45+}
46+
47+func validBrokerSnapshot() BrokerSnapshot {
48+ return BrokerSnapshot{
49+ Epoch: 3,
50+ Revision: 9,
51+ Hosts: []RemoteHostSnapshot{testBrokerHost("user@arch")},
52+ }
53+}
54+
55+func TestBrokerSnapshotValidateBounds(t *testing.T) {
56+ tests := []struct {
57+ name string
58+ mutate func(*BrokerSnapshot)
59+ wantErr bool
60+ }{
61+ {name: "valid", mutate: func(*BrokerSnapshot) {}, wantErr: false},
62+ {name: "zero epoch", mutate: func(s *BrokerSnapshot) { s.Epoch = 0 }, wantErr: true},
63+ {name: "zero revision", mutate: func(s *BrokerSnapshot) { s.Revision = 0 }, wantErr: true},
64+ {name: "too many hosts", mutate: func(s *BrokerSnapshot) {
65+ s.Hosts = make([]RemoteHostSnapshot, BrokerMaxHosts+1)
66+ for i := range s.Hosts {
67+ s.Hosts[i] = testBrokerHost("user@arch")
68+ s.Hosts[i].Registration = testBrokerRegistration("user@arch", 1, 1)
69+ }
70+ }, wantErr: true},
71+ {name: "duplicate host", mutate: func(s *BrokerSnapshot) {
72+ s.Hosts = append(s.Hosts, testBrokerHost("user@arch"))
73+ }, wantErr: true},
74+ {name: "endpoint registration mismatch", mutate: func(s *BrokerSnapshot) {
75+ s.Hosts[0].Registration = testBrokerRegistration("user@other", 1, 1)
76+ }, wantErr: true},
77+ {name: "zero registration incarnation", mutate: func(s *BrokerSnapshot) {
78+ s.Hosts[0].Registration = domain.RemoteRegistration{Endpoint: "user@arch", Generation: 1}
79+ }, wantErr: true},
80+ {name: "live host as tombstone", mutate: func(s *BrokerSnapshot) {
81+ s.Removed = []BrokerHostTombstone{{
82+ Endpoint: "user@arch",
83+ Registration: testBrokerRegistration("user@arch", 1, 1),
84+ RetiredRevision: 9,
85+ }}
86+ }, wantErr: true},
87+ {name: "too many tombstones", mutate: func(s *BrokerSnapshot) {
88+ s.Removed = make([]BrokerHostTombstone, BrokerMaxTombstones+1)
89+ for i := range s.Removed {
90+ s.Removed[i] = BrokerHostTombstone{
91+ Endpoint: "user@gone",
92+ Registration: testBrokerRegistration("user@gone", 1, 1),
93+ RetiredRevision: 9,
94+ }
95+ }
96+ }, wantErr: true},
97+ }
98+ for _, tt := range tests {
99+ t.Run(tt.name, func(t *testing.T) {
100+ snapshot := validBrokerSnapshot()
101+ tt.mutate(&snapshot)
102+ if err := snapshot.Validate(); (err != nil) != tt.wantErr {
103+ t.Fatalf("Validate() = %v, wantErr %t", err, tt.wantErr)
104+ }
105+ })
106+ }
107+}
108+
109+func TestBrokerSnapshotEpochRevisionOrdering(t *testing.T) {
110+ tests := []struct {
111+ name string
112+ current BrokerSnapshot
113+ candidate BrokerSnapshot
114+ wantNewer bool
115+ description string
116+ }{
117+ {
118+ name: "higher revision wins in epoch",
119+ current: BrokerSnapshot{Epoch: 3, Revision: 9},
120+ candidate: BrokerSnapshot{Epoch: 3, Revision: 10},
121+ wantNewer: true,
122+ description: "monotonic revision within an epoch",
123+ },
124+ {
125+ name: "lower revision loses in epoch",
126+ current: BrokerSnapshot{Epoch: 3, Revision: 10},
127+ candidate: BrokerSnapshot{Epoch: 3, Revision: 9},
128+ wantNewer: false,
129+ description: "stale revision never applies",
130+ },
131+ {
132+ name: "equal revision loses",
133+ current: BrokerSnapshot{Epoch: 3, Revision: 9},
134+ candidate: BrokerSnapshot{Epoch: 3, Revision: 9},
135+ wantNewer: false,
136+ description: "replay of the same revision is not newer",
137+ },
138+ {
139+ name: "epoch change forces resync",
140+ current: BrokerSnapshot{Epoch: 3, Revision: 99},
141+ candidate: BrokerSnapshot{Epoch: 4, Revision: 1},
142+ wantNewer: true,
143+ description: "any valid epoch change supersedes regardless of revision",
144+ },
145+ {
146+ name: "lower epoch still forces resync",
147+ current: BrokerSnapshot{Epoch: 9, Revision: 1},
148+ candidate: BrokerSnapshot{Epoch: 4, Revision: 50},
149+ wantNewer: true,
150+ description: "epochs are fresh per process, never numerically ordered",
151+ },
152+ {
153+ name: "zero epoch never supersedes",
154+ current: BrokerSnapshot{Epoch: 3, Revision: 9},
155+ candidate: BrokerSnapshot{},
156+ wantNewer: false,
157+ description: "invalid snapshots never apply",
158+ },
159+ }
160+ for _, tt := range tests {
161+ t.Run(tt.name, func(t *testing.T) {
162+ if got := tt.candidate.Supersedes(tt.current); got != tt.wantNewer {
163+ t.Fatalf("Supersedes() = %t, want %t (%s)", got, tt.wantNewer, tt.description)
164+ }
165+ })
166+ }
167+}
168+
169+func TestBrokerSnapshotCloneAndFind(t *testing.T) {
170+ original := validBrokerSnapshot()
171+ original.Hosts[0].Sessions = []catalogue.RemoteCatalogSession{{
172+ LifecycleID: testBrokerLifecycle(),
173+ Name: "work",
174+ State: catalogue.RemoteCatalogSessionUp,
175+ Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "shell"}},
176+ }}
177+ cloned := original.Clone()
178+ if found, ok := cloned.Find("user@arch"); !ok || found.Endpoint != "user@arch" {
179+ t.Fatalf("Find() = %+v, %t, want the host", found, ok)
180+ }
181+ if _, ok := cloned.Find("user@mule"); ok {
182+ t.Fatal("Find() must miss unknown endpoints")
183+ }
184+ cloned.Hosts[0].Sessions[0].Name = "mutated"
185+ cloned.Hosts[0].Sessions[0].Tabs[0].ID = "mutated"
186+ if original.Hosts[0].Sessions[0].Name != "work" || original.Hosts[0].Sessions[0].Tabs[0].ID != "tab-1" {
187+ t.Fatal("Clone() shares memory with the original")
188+ }
189+}
190+
191+func TestBrokerTombstoneFencing(t *testing.T) {
192+ registration := testBrokerRegistration("user@arch", 4, 2)
193+ tombstone := BrokerHostTombstone{
194+ Endpoint: "user@arch",
195+ Registration: registration,
196+ RetiredRevision: 12,
197+ }
198+ if err := tombstone.Validate(); err != nil {
199+ t.Fatalf("Validate() = %v", err)
200+ }
201+ tests := []struct {
202+ name string
203+ candidate domain.RemoteRegistration
204+ wantFence bool
205+ }{
206+ {name: "same registration fenced", candidate: registration, wantFence: true},
207+ {name: "older generation fenced", candidate: testBrokerRegistration("user@arch", 4, 1), wantFence: true},
208+ {name: "fresh incarnation not fenced", candidate: testBrokerRegistration("user@arch", 5, 1), wantFence: false},
209+ {name: "other endpoint not fenced", candidate: testBrokerRegistration("user@mule", 4, 2), wantFence: false},
210+ }
211+ for _, tt := range tests {
212+ t.Run(tt.name, func(t *testing.T) {
213+ if got := tombstone.Fences(tt.candidate); got != tt.wantFence {
214+ t.Fatalf("Fences() = %t, want %t", got, tt.wantFence)
215+ }
216+ })
217+ }
218+
219+ invalid := []BrokerHostTombstone{
220+ {Registration: registration, RetiredRevision: 1},
221+ {Endpoint: "user@arch", Registration: testBrokerRegistration("user@other", 4, 2), RetiredRevision: 1},
222+ {Endpoint: "user@arch", Registration: registration},
223+ }
224+ for i, tomb := range invalid {
225+ if err := tomb.Validate(); err == nil {
226+ t.Fatalf("invalid tombstone %d accepted", i)
227+ }
228+ }
229+}
230+
231+func TestBrokerCompletionStaleRejection(t *testing.T) {
232+ current := testBrokerRegistration("user@arch", 4, 2)
233+ tests := []struct {
234+ name string
235+ currentEpoch BrokerEpoch
236+ completion domain.RemoteRegistration
237+ completionEp BrokerEpoch
238+ wantStale bool
239+ }{
240+ {name: "exact match applies", currentEpoch: 3, completion: current, completionEp: 3, wantStale: false},
241+ {name: "epoch change stale", currentEpoch: 4, completion: current, completionEp: 3, wantStale: true},
242+ {name: "generation bump stalls old", currentEpoch: 3, completion: testBrokerRegistration("user@arch", 4, 1), completionEp: 3, wantStale: true},
243+ {name: "re-add incarnation stalls old", currentEpoch: 3, completion: testBrokerRegistration("user@arch", 5, 1), completionEp: 3, wantStale: true},
244+ {name: "zero epoch stale", currentEpoch: 0, completion: current, completionEp: 3, wantStale: true},
245+ {name: "zero completion epoch stale", currentEpoch: 3, completion: current, completionEp: 0, wantStale: true},
246+ }
247+ for _, tt := range tests {
248+ t.Run(tt.name, func(t *testing.T) {
249+ if got := BrokerCompletionIsStale(current, tt.currentEpoch, tt.completion, tt.completionEp); got != tt.wantStale {
250+ t.Fatalf("BrokerCompletionIsStale() = %t, want %t", got, tt.wantStale)
251+ }
252+ })
253+ }
254+}
255+
256+func TestBrokerPolicyCompatibility(t *testing.T) {
257+ base := testBrokerPolicy()
258+ if err := base.Validate(); err != nil {
259+ t.Fatalf("Validate() = %v", err)
260+ }
261+ if !base.Compatible(base) {
262+ t.Fatal("identical policy must be compatible")
263+ }
264+ conflicts := map[string]BrokerPolicy{
265+ "protocol version": withBrokerPolicy(base, func(p *BrokerPolicy) { p.ProtocolVersion++ }),
266+ "catalog schema": withBrokerPolicy(base, func(p *BrokerPolicy) { p.CatalogSchemaVersion++ }),
267+ "environment": withBrokerPolicy(base, func(p *BrokerPolicy) { p.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned }),
268+ "transport": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Transport = "stdio" }),
269+ }
270+ for name, conflict := range conflicts {
271+ if base.Compatible(conflict) {
272+ t.Fatalf("conflicting policy %q must be rejected, never merged", name)
273+ }
274+ }
275+ invalid := []BrokerPolicy{
276+ {},
277+ {ProtocolVersion: protocol.Version},
278+ {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion},
279+ {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicy(9), Transport: "quic"},
280+ {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned},
281+ }
282+ for i, policy := range invalid {
283+ if err := policy.Validate(); err == nil {
284+ t.Fatalf("invalid policy %d accepted", i)
285+ }
286+ }
287+}
288+
289+func TestBrokerOpenStreamRequestValidate(t *testing.T) {
290+ valid := func() BrokerOpenStreamRequest {
291+ return BrokerOpenStreamRequest{
292+ Connection: BrokerConnectionID{1},
293+ Stream: 2,
294+ Endpoint: "user@arch",
295+ Registration: testBrokerRegistration("user@arch", 4, 2),
296+ Target: testBrokerTarget(),
297+ Env: []string{"TERM=xterm-256color"},
298+ Policy: testBrokerPolicy(),
299+ }
300+ }
301+ if err := valid().Validate(); err != nil {
302+ t.Fatalf("Validate() = %v", err)
303+ }
304+ tests := []struct {
305+ name string
306+ mutate func(*BrokerOpenStreamRequest)
307+ wantErr string
308+ }{
309+ {name: "zero connection", mutate: func(r *BrokerOpenStreamRequest) { r.Connection = BrokerConnectionID{} }},
310+ {name: "zero stream", mutate: func(r *BrokerOpenStreamRequest) { r.Stream = 0 }},
311+ {name: "empty endpoint", mutate: func(r *BrokerOpenStreamRequest) { r.Endpoint = "" }},
312+ {name: "endpoint registration mismatch", mutate: func(r *BrokerOpenStreamRequest) { r.Registration = testBrokerRegistration("user@other", 4, 2) }},
313+ {name: "zero target lifecycle", mutate: func(r *BrokerOpenStreamRequest) {
314+ r.Target = protocol.ExactSessionTarget{SessionName: "work"}
315+ }},
316+ {name: "bad session name", mutate: func(r *BrokerOpenStreamRequest) {
317+ r.Target = protocol.ExactSessionTarget{LifecycleID: testBrokerLifecycle(), SessionName: "bad name!"}
318+ }},
319+ {name: "too many env entries", mutate: func(r *BrokerOpenStreamRequest) {
320+ r.Env = make([]string, BrokerMaxEnvEntries+1)
321+ }},
322+ {name: "oversize env entry", mutate: func(r *BrokerOpenStreamRequest) {
323+ r.Env = []string{strings.Repeat("A", BrokerMaxEnvEntryBytes+1)}
324+ }},
325+ {name: "invalid env encoding", mutate: func(r *BrokerOpenStreamRequest) {
326+ r.Env = []string{"TERM=\xff"}
327+ }},
328+ {name: "invalid policy", mutate: func(r *BrokerOpenStreamRequest) { r.Policy = BrokerPolicy{} }},
329+ }
330+ for _, tt := range tests {
331+ t.Run(tt.name, func(t *testing.T) {
332+ request := valid()
333+ tt.mutate(&request)
334+ if err := request.Validate(); err == nil {
335+ t.Fatal("invalid open-stream request accepted")
336+ }
337+ })
338+ }
339+}
340+
341+func TestBrokerMutationOutcomeUncertainty(t *testing.T) {
342+ for _, outcome := range []BrokerMutationOutcome{BrokerOutcomeOK, BrokerOutcomeFailed, BrokerOutcomeUnknown} {
343+ if err := outcome.Validate(); err != nil {
344+ t.Fatalf("Validate(%v) = %v", outcome, err)
345+ }
346+ }
347+ if err := BrokerMutationOutcome(0).Validate(); err == nil {
348+ t.Fatal("zero mutation outcome accepted")
349+ }
350+ if BrokerOutcomeOK.String() != "ok" || BrokerOutcomeUnknown.String() != "outcome_unknown" {
351+ t.Fatal("mutation outcome tokens changed")
352+ }
353+
354+ valid := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeFailed, Text: "dial refused"}
355+ if err := valid.Validate(); err != nil {
356+ t.Fatalf("Validate() = %v", err)
357+ }
358+ unknown := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeUnknown, Text: "lost after commit"}
359+ if err := unknown.Validate(); err != nil {
360+ t.Fatalf("outcome-unknown result must stay valid: %v", err)
361+ }
362+ okWithText := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeOK, Text: "extra"}
363+ if err := okWithText.Validate(); err == nil {
364+ t.Fatal("successful result must not carry text")
365+ }
366+ oversize := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeFailed, Text: strings.Repeat("e", BrokerMaxErrorBytes+1)}
367+ if err := oversize.Validate(); err == nil {
368+ t.Fatal("oversize result text accepted")
369+ }
370+ unsafe := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeFailed, Text: "dial\x1b[31m refused"}
371+ if err := unsafe.Validate(); err == nil {
372+ t.Fatal("terminal control sequence in operation result accepted")
373+ }
374+}
375+
376+func TestBrokerErrorTerminalOutcomes(t *testing.T) {
377+ tests := []struct {
378+ code BrokerErrorCode
379+ wantTerminal bool
380+ }{
381+ {code: BrokerErrorUnavailable, wantTerminal: false},
382+ {code: BrokerErrorIncompatible, wantTerminal: false},
383+ {code: BrokerErrorTimeout, wantTerminal: false},
384+ {code: BrokerErrorCancelled, wantTerminal: false},
385+ {code: BrokerErrorOutcomeUnknown, wantTerminal: false},
386+ {code: BrokerErrorAttachmentLost, wantTerminal: false},
387+ {code: BrokerErrorStaleEpoch, wantTerminal: false},
388+ {code: BrokerErrorConflictingPolicy, wantTerminal: false},
389+ {code: BrokerErrorExplicitExit, wantTerminal: true},
390+ {code: BrokerErrorFatalTerminal, wantTerminal: true},
391+ }
392+ for _, tt := range tests {
393+ t.Run(tt.code.String(), func(t *testing.T) {
394+ failure := BrokerError{Code: tt.code, Text: "visible typed error"}
395+ if err := failure.Validate(); err != nil {
396+ t.Fatalf("Validate() = %v", err)
397+ }
398+ if got := failure.Terminal(); got != tt.wantTerminal {
399+ t.Fatalf("Terminal() = %t, want %t", got, tt.wantTerminal)
400+ }
401+ if failure.Error() == "" {
402+ t.Fatal("Error() must render a message")
403+ }
404+ })
405+ }
406+ if err := (BrokerError{Code: BrokerErrorCode(0)}).Validate(); err == nil {
407+ t.Fatal("zero broker error code accepted")
408+ }
409+ oversize := BrokerError{Code: BrokerErrorTimeout, Text: strings.Repeat("e", BrokerMaxErrorBytes+1)}
410+ if err := oversize.Validate(); err == nil {
411+ t.Fatal("oversize broker error text accepted")
412+ }
413+ unsafe := BrokerError{Code: BrokerErrorTimeout, Text: "timeout\u202erorrE"}
414+ if err := unsafe.Validate(); err == nil {
415+ t.Fatal("bidirectional control in broker error accepted")
416+ }
417+}
418+
419+func TestBrokerIdentityScopesStayDistinct(t *testing.T) {
420+ // Every identity level validates independently and zero values never
421+ // identify anything: connection, stream, operation, daemon incarnation,
422+ // registration, session lifecycle, and broker epoch are distinct types.
423+ if err := (BrokerConnectionID{}).Validate(); err == nil {
424+ t.Fatal("zero connection ID accepted")
425+ }
426+ if err := BrokerStreamID(0).Validate(); err == nil {
427+ t.Fatal("zero stream ID accepted")
428+ }
429+ if err := (BrokerOperationID{}).Validate(); err == nil {
430+ t.Fatal("zero operation ID accepted")
431+ }
432+ if err := (BrokerDaemonIncarnation{}).Validate(); err == nil {
433+ t.Fatal("zero daemon incarnation accepted")
434+ }
435+ if err := (domain.RemoteRegistration{}).Validate(); err == nil {
436+ t.Fatal("zero host registration accepted")
437+ }
438+ if err := (protocol.ExactSessionTarget{}).Validate(); err == nil {
439+ t.Fatal("zero session lifecycle target accepted")
440+ }
441+ if err := BrokerDaemonIdentity("").Validate(); err == nil {
442+ t.Fatal("empty daemon identity accepted")
443+ }
444+ if err := (BrokerStreamLost{}).Validate(); err == nil {
445+ t.Fatal("zero stream loss accepted")
446+ }
447+ loss := BrokerStreamLost{
448+ Connection: BrokerConnectionID{1},
449+ Stream: 2,
450+ Epoch: 3,
451+ Cause: domain.RemoteFailureTimeout,
452+ }
453+ if err := loss.Validate(); err != nil {
454+ t.Fatalf("Validate() = %v", err)
455+ }
456+}
457+
458+func withBrokerPolicy(base BrokerPolicy, mutate func(*BrokerPolicy)) BrokerPolicy {
459+ mutate(&base)
460+ return base
461+}
462+
463+func TestBrokerInterfacesArePorts(t *testing.T) {
464+ // Broker, client, and daemon cross-layer contracts are ports: the
465+ // seams must stay interfaces so Mockery can generate mocks and
466+ // adapters can implement them without importing use cases.
467+ for name, iface := range map[string]reflect.Type{
468+ "BrokerService": reflect.TypeOf((*BrokerService)(nil)).Elem(),
469+ "BrokerSnapshotStore": reflect.TypeOf((*BrokerSnapshotStore)(nil)).Elem(),
470+ "BrokerPhysicalConnection": reflect.TypeOf((*BrokerPhysicalConnection)(nil)).Elem(),
471+ "BrokerEndpointConnector": reflect.TypeOf((*BrokerEndpointConnector)(nil)).Elem(),
472+ "BrokerListener": reflect.TypeOf((*BrokerListener)(nil)).Elem(),
473+ "BrokerSubscription": reflect.TypeOf((*BrokerSubscription)(nil)).Elem(),
474+ } {
475+ if iface.Kind() != reflect.Interface {
476+ t.Fatalf("%s must stay an interface, got %v", name, iface.Kind())
477+ }
478+ }
479+}
@@ -0,0 +1,479 @@
1+package ports
2+
3+import (
4+ "reflect"
5+ "strings"
6+ "testing"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
10+ "github.com/bnema/vev/internal/protocol/catalogue"
11+)
12+
13+func testBrokerRegistration(endpoint string, incarnation byte, generation domain.RemoteGeneration) domain.RemoteRegistration {
14+ return domain.RemoteRegistration{
15+ Endpoint: endpoint,
16+ Incarnation: [16]byte{incarnation, 7, 9},
17+ Generation: generation,
18+ }
19+}
20+
21+func testBrokerLifecycle() domain.SessionLifecycleID {
22+ return domain.SessionLifecycleID{1, 2, 3}
23+}
24+
25+func testBrokerTarget() protocol.ExactSessionTarget {
26+ return protocol.ExactSessionTarget{LifecycleID: testBrokerLifecycle(), SessionName: "work"}
27+}
28+
29+func testBrokerPolicy() BrokerPolicy {
30+ return BrokerPolicy{
31+ ProtocolVersion: protocol.Version,
32+ CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion,
33+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
34+ Transport: "quic",
35+ }
36+}
37+
38+func testBrokerHost(endpoint string) RemoteHostSnapshot {
39+ return RemoteHostSnapshot{
40+ Endpoint: endpoint,
41+ DisplayOrigin: endpoint,
42+ Registration: testBrokerRegistration(endpoint, 1, 1),
43+ Availability: domain.RemoteAvailabilityReachable,
44+ }
45+}
46+
47+func validBrokerSnapshot() BrokerSnapshot {
48+ return BrokerSnapshot{
49+ Epoch: 3,
50+ Revision: 9,
51+ Hosts: []RemoteHostSnapshot{testBrokerHost("user@arch")},
52+ }
53+}
54+
55+func TestBrokerSnapshotValidateBounds(t *testing.T) {
56+ tests := []struct {
57+ name string
58+ mutate func(*BrokerSnapshot)
59+ wantErr bool
60+ }{
61+ {name: "valid", mutate: func(*BrokerSnapshot) {}, wantErr: false},
62+ {name: "zero epoch", mutate: func(s *BrokerSnapshot) { s.Epoch = 0 }, wantErr: true},
63+ {name: "zero revision", mutate: func(s *BrokerSnapshot) { s.Revision = 0 }, wantErr: true},
64+ {name: "too many hosts", mutate: func(s *BrokerSnapshot) {
65+ s.Hosts = make([]RemoteHostSnapshot, BrokerMaxHosts+1)
66+ for i := range s.Hosts {
67+ s.Hosts[i] = testBrokerHost("user@arch")
68+ s.Hosts[i].Registration = testBrokerRegistration("user@arch", 1, 1)
69+ }
70+ }, wantErr: true},
71+ {name: "duplicate host", mutate: func(s *BrokerSnapshot) {
72+ s.Hosts = append(s.Hosts, testBrokerHost("user@arch"))
73+ }, wantErr: true},
74+ {name: "endpoint registration mismatch", mutate: func(s *BrokerSnapshot) {
75+ s.Hosts[0].Registration = testBrokerRegistration("user@other", 1, 1)
76+ }, wantErr: true},
77+ {name: "zero registration incarnation", mutate: func(s *BrokerSnapshot) {
78+ s.Hosts[0].Registration = domain.RemoteRegistration{Endpoint: "user@arch", Generation: 1}
79+ }, wantErr: true},
80+ {name: "live host as tombstone", mutate: func(s *BrokerSnapshot) {
81+ s.Removed = []BrokerHostTombstone{{
82+ Endpoint: "user@arch",
83+ Registration: testBrokerRegistration("user@arch", 1, 1),
84+ RetiredRevision: 9,
85+ }}
86+ }, wantErr: true},
87+ {name: "too many tombstones", mutate: func(s *BrokerSnapshot) {
88+ s.Removed = make([]BrokerHostTombstone, BrokerMaxTombstones+1)
89+ for i := range s.Removed {
90+ s.Removed[i] = BrokerHostTombstone{
91+ Endpoint: "user@gone",
92+ Registration: testBrokerRegistration("user@gone", 1, 1),
93+ RetiredRevision: 9,
94+ }
95+ }
96+ }, wantErr: true},
97+ }
98+ for _, tt := range tests {
99+ t.Run(tt.name, func(t *testing.T) {
100+ snapshot := validBrokerSnapshot()
101+ tt.mutate(&snapshot)
102+ if err := snapshot.Validate(); (err != nil) != tt.wantErr {
103+ t.Fatalf("Validate() = %v, wantErr %t", err, tt.wantErr)
104+ }
105+ })
106+ }
107+}
108+
109+func TestBrokerSnapshotEpochRevisionOrdering(t *testing.T) {
110+ tests := []struct {
111+ name string
112+ current BrokerSnapshot
113+ candidate BrokerSnapshot
114+ wantNewer bool
115+ description string
116+ }{
117+ {
118+ name: "higher revision wins in epoch",
119+ current: BrokerSnapshot{Epoch: 3, Revision: 9},
120+ candidate: BrokerSnapshot{Epoch: 3, Revision: 10},
121+ wantNewer: true,
122+ description: "monotonic revision within an epoch",
123+ },
124+ {
125+ name: "lower revision loses in epoch",
126+ current: BrokerSnapshot{Epoch: 3, Revision: 10},
127+ candidate: BrokerSnapshot{Epoch: 3, Revision: 9},
128+ wantNewer: false,
129+ description: "stale revision never applies",
130+ },
131+ {
132+ name: "equal revision loses",
133+ current: BrokerSnapshot{Epoch: 3, Revision: 9},
134+ candidate: BrokerSnapshot{Epoch: 3, Revision: 9},
135+ wantNewer: false,
136+ description: "replay of the same revision is not newer",
137+ },
138+ {
139+ name: "epoch change forces resync",
140+ current: BrokerSnapshot{Epoch: 3, Revision: 99},
141+ candidate: BrokerSnapshot{Epoch: 4, Revision: 1},
142+ wantNewer: true,
143+ description: "any valid epoch change supersedes regardless of revision",
144+ },
145+ {
146+ name: "lower epoch still forces resync",
147+ current: BrokerSnapshot{Epoch: 9, Revision: 1},
148+ candidate: BrokerSnapshot{Epoch: 4, Revision: 50},
149+ wantNewer: true,
150+ description: "epochs are fresh per process, never numerically ordered",
151+ },
152+ {
153+ name: "zero epoch never supersedes",
154+ current: BrokerSnapshot{Epoch: 3, Revision: 9},
155+ candidate: BrokerSnapshot{},
156+ wantNewer: false,
157+ description: "invalid snapshots never apply",
158+ },
159+ }
160+ for _, tt := range tests {
161+ t.Run(tt.name, func(t *testing.T) {
162+ if got := tt.candidate.Supersedes(tt.current); got != tt.wantNewer {
163+ t.Fatalf("Supersedes() = %t, want %t (%s)", got, tt.wantNewer, tt.description)
164+ }
165+ })
166+ }
167+}
168+
169+func TestBrokerSnapshotCloneAndFind(t *testing.T) {
170+ original := validBrokerSnapshot()
171+ original.Hosts[0].Sessions = []catalogue.RemoteCatalogSession{{
172+ LifecycleID: testBrokerLifecycle(),
173+ Name: "work",
174+ State: catalogue.RemoteCatalogSessionUp,
175+ Tabs: []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "shell"}},
176+ }}
177+ cloned := original.Clone()
178+ if found, ok := cloned.Find("user@arch"); !ok || found.Endpoint != "user@arch" {
179+ t.Fatalf("Find() = %+v, %t, want the host", found, ok)
180+ }
181+ if _, ok := cloned.Find("user@mule"); ok {
182+ t.Fatal("Find() must miss unknown endpoints")
183+ }
184+ cloned.Hosts[0].Sessions[0].Name = "mutated"
185+ cloned.Hosts[0].Sessions[0].Tabs[0].ID = "mutated"
186+ if original.Hosts[0].Sessions[0].Name != "work" || original.Hosts[0].Sessions[0].Tabs[0].ID != "tab-1" {
187+ t.Fatal("Clone() shares memory with the original")
188+ }
189+}
190+
191+func TestBrokerTombstoneFencing(t *testing.T) {
192+ registration := testBrokerRegistration("user@arch", 4, 2)
193+ tombstone := BrokerHostTombstone{
194+ Endpoint: "user@arch",
195+ Registration: registration,
196+ RetiredRevision: 12,
197+ }
198+ if err := tombstone.Validate(); err != nil {
199+ t.Fatalf("Validate() = %v", err)
200+ }
201+ tests := []struct {
202+ name string
203+ candidate domain.RemoteRegistration
204+ wantFence bool
205+ }{
206+ {name: "same registration fenced", candidate: registration, wantFence: true},
207+ {name: "older generation fenced", candidate: testBrokerRegistration("user@arch", 4, 1), wantFence: true},
208+ {name: "fresh incarnation not fenced", candidate: testBrokerRegistration("user@arch", 5, 1), wantFence: false},
209+ {name: "other endpoint not fenced", candidate: testBrokerRegistration("user@mule", 4, 2), wantFence: false},
210+ }
211+ for _, tt := range tests {
212+ t.Run(tt.name, func(t *testing.T) {
213+ if got := tombstone.Fences(tt.candidate); got != tt.wantFence {
214+ t.Fatalf("Fences() = %t, want %t", got, tt.wantFence)
215+ }
216+ })
217+ }
218+
219+ invalid := []BrokerHostTombstone{
220+ {Registration: registration, RetiredRevision: 1},
221+ {Endpoint: "user@arch", Registration: testBrokerRegistration("user@other", 4, 2), RetiredRevision: 1},
222+ {Endpoint: "user@arch", Registration: registration},
223+ }
224+ for i, tomb := range invalid {
225+ if err := tomb.Validate(); err == nil {
226+ t.Fatalf("invalid tombstone %d accepted", i)
227+ }
228+ }
229+}
230+
231+func TestBrokerCompletionStaleRejection(t *testing.T) {
232+ current := testBrokerRegistration("user@arch", 4, 2)
233+ tests := []struct {
234+ name string
235+ currentEpoch BrokerEpoch
236+ completion domain.RemoteRegistration
237+ completionEp BrokerEpoch
238+ wantStale bool
239+ }{
240+ {name: "exact match applies", currentEpoch: 3, completion: current, completionEp: 3, wantStale: false},
241+ {name: "epoch change stale", currentEpoch: 4, completion: current, completionEp: 3, wantStale: true},
242+ {name: "generation bump stalls old", currentEpoch: 3, completion: testBrokerRegistration("user@arch", 4, 1), completionEp: 3, wantStale: true},
243+ {name: "re-add incarnation stalls old", currentEpoch: 3, completion: testBrokerRegistration("user@arch", 5, 1), completionEp: 3, wantStale: true},
244+ {name: "zero epoch stale", currentEpoch: 0, completion: current, completionEp: 3, wantStale: true},
245+ {name: "zero completion epoch stale", currentEpoch: 3, completion: current, completionEp: 0, wantStale: true},
246+ }
247+ for _, tt := range tests {
248+ t.Run(tt.name, func(t *testing.T) {
249+ if got := BrokerCompletionIsStale(current, tt.currentEpoch, tt.completion, tt.completionEp); got != tt.wantStale {
250+ t.Fatalf("BrokerCompletionIsStale() = %t, want %t", got, tt.wantStale)
251+ }
252+ })
253+ }
254+}
255+
256+func TestBrokerPolicyCompatibility(t *testing.T) {
257+ base := testBrokerPolicy()
258+ if err := base.Validate(); err != nil {
259+ t.Fatalf("Validate() = %v", err)
260+ }
261+ if !base.Compatible(base) {
262+ t.Fatal("identical policy must be compatible")
263+ }
264+ conflicts := map[string]BrokerPolicy{
265+ "protocol version": withBrokerPolicy(base, func(p *BrokerPolicy) { p.ProtocolVersion++ }),
266+ "catalog schema": withBrokerPolicy(base, func(p *BrokerPolicy) { p.CatalogSchemaVersion++ }),
267+ "environment": withBrokerPolicy(base, func(p *BrokerPolicy) { p.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned }),
268+ "transport": withBrokerPolicy(base, func(p *BrokerPolicy) { p.Transport = "stdio" }),
269+ }
270+ for name, conflict := range conflicts {
271+ if base.Compatible(conflict) {
272+ t.Fatalf("conflicting policy %q must be rejected, never merged", name)
273+ }
274+ }
275+ invalid := []BrokerPolicy{
276+ {},
277+ {ProtocolVersion: protocol.Version},
278+ {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion},
279+ {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicy(9), Transport: "quic"},
280+ {ProtocolVersion: protocol.Version, CatalogSchemaVersion: catalogue.RemoteCatalogSchemaVersion, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned},
281+ }
282+ for i, policy := range invalid {
283+ if err := policy.Validate(); err == nil {
284+ t.Fatalf("invalid policy %d accepted", i)
285+ }
286+ }
287+}
288+
289+func TestBrokerOpenStreamRequestValidate(t *testing.T) {
290+ valid := func() BrokerOpenStreamRequest {
291+ return BrokerOpenStreamRequest{
292+ Connection: BrokerConnectionID{1},
293+ Stream: 2,
294+ Endpoint: "user@arch",
295+ Registration: testBrokerRegistration("user@arch", 4, 2),
296+ Target: testBrokerTarget(),
297+ Env: []string{"TERM=xterm-256color"},
298+ Policy: testBrokerPolicy(),
299+ }
300+ }
301+ if err := valid().Validate(); err != nil {
302+ t.Fatalf("Validate() = %v", err)
303+ }
304+ tests := []struct {
305+ name string
306+ mutate func(*BrokerOpenStreamRequest)
307+ wantErr string
308+ }{
309+ {name: "zero connection", mutate: func(r *BrokerOpenStreamRequest) { r.Connection = BrokerConnectionID{} }},
310+ {name: "zero stream", mutate: func(r *BrokerOpenStreamRequest) { r.Stream = 0 }},
311+ {name: "empty endpoint", mutate: func(r *BrokerOpenStreamRequest) { r.Endpoint = "" }},
312+ {name: "endpoint registration mismatch", mutate: func(r *BrokerOpenStreamRequest) { r.Registration = testBrokerRegistration("user@other", 4, 2) }},
313+ {name: "zero target lifecycle", mutate: func(r *BrokerOpenStreamRequest) {
314+ r.Target = protocol.ExactSessionTarget{SessionName: "work"}
315+ }},
316+ {name: "bad session name", mutate: func(r *BrokerOpenStreamRequest) {
317+ r.Target = protocol.ExactSessionTarget{LifecycleID: testBrokerLifecycle(), SessionName: "bad name!"}
318+ }},
319+ {name: "too many env entries", mutate: func(r *BrokerOpenStreamRequest) {
320+ r.Env = make([]string, BrokerMaxEnvEntries+1)
321+ }},
322+ {name: "oversize env entry", mutate: func(r *BrokerOpenStreamRequest) {
323+ r.Env = []string{strings.Repeat("A", BrokerMaxEnvEntryBytes+1)}
324+ }},
325+ {name: "invalid env encoding", mutate: func(r *BrokerOpenStreamRequest) {
326+ r.Env = []string{"TERM=\xff"}
327+ }},
328+ {name: "invalid policy", mutate: func(r *BrokerOpenStreamRequest) { r.Policy = BrokerPolicy{} }},
329+ }
330+ for _, tt := range tests {
331+ t.Run(tt.name, func(t *testing.T) {
332+ request := valid()
333+ tt.mutate(&request)
334+ if err := request.Validate(); err == nil {
335+ t.Fatal("invalid open-stream request accepted")
336+ }
337+ })
338+ }
339+}
340+
341+func TestBrokerMutationOutcomeUncertainty(t *testing.T) {
342+ for _, outcome := range []BrokerMutationOutcome{BrokerOutcomeOK, BrokerOutcomeFailed, BrokerOutcomeUnknown} {
343+ if err := outcome.Validate(); err != nil {
344+ t.Fatalf("Validate(%v) = %v", outcome, err)
345+ }
346+ }
347+ if err := BrokerMutationOutcome(0).Validate(); err == nil {
348+ t.Fatal("zero mutation outcome accepted")
349+ }
350+ if BrokerOutcomeOK.String() != "ok" || BrokerOutcomeUnknown.String() != "outcome_unknown" {
351+ t.Fatal("mutation outcome tokens changed")
352+ }
353+
354+ valid := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeFailed, Text: "dial refused"}
355+ if err := valid.Validate(); err != nil {
356+ t.Fatalf("Validate() = %v", err)
357+ }
358+ unknown := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeUnknown, Text: "lost after commit"}
359+ if err := unknown.Validate(); err != nil {
360+ t.Fatalf("outcome-unknown result must stay valid: %v", err)
361+ }
362+ okWithText := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeOK, Text: "extra"}
363+ if err := okWithText.Validate(); err == nil {
364+ t.Fatal("successful result must not carry text")
365+ }
366+ oversize := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeFailed, Text: strings.Repeat("e", BrokerMaxErrorBytes+1)}
367+ if err := oversize.Validate(); err == nil {
368+ t.Fatal("oversize result text accepted")
369+ }
370+ unsafe := BrokerOperationResult{Operation: BrokerOperationID{1}, Outcome: BrokerOutcomeFailed, Text: "dial\x1b[31m refused"}
371+ if err := unsafe.Validate(); err == nil {
372+ t.Fatal("terminal control sequence in operation result accepted")
373+ }
374+}
375+
376+func TestBrokerErrorTerminalOutcomes(t *testing.T) {
377+ tests := []struct {
378+ code BrokerErrorCode
379+ wantTerminal bool
380+ }{
381+ {code: BrokerErrorUnavailable, wantTerminal: false},
382+ {code: BrokerErrorIncompatible, wantTerminal: false},
383+ {code: BrokerErrorTimeout, wantTerminal: false},
384+ {code: BrokerErrorCancelled, wantTerminal: false},
385+ {code: BrokerErrorOutcomeUnknown, wantTerminal: false},
386+ {code: BrokerErrorAttachmentLost, wantTerminal: false},
387+ {code: BrokerErrorStaleEpoch, wantTerminal: false},
388+ {code: BrokerErrorConflictingPolicy, wantTerminal: false},
389+ {code: BrokerErrorExplicitExit, wantTerminal: true},
390+ {code: BrokerErrorFatalTerminal, wantTerminal: true},
391+ }
392+ for _, tt := range tests {
393+ t.Run(tt.code.String(), func(t *testing.T) {
394+ failure := BrokerError{Code: tt.code, Text: "visible typed error"}
395+ if err := failure.Validate(); err != nil {
396+ t.Fatalf("Validate() = %v", err)
397+ }
398+ if got := failure.Terminal(); got != tt.wantTerminal {
399+ t.Fatalf("Terminal() = %t, want %t", got, tt.wantTerminal)
400+ }
401+ if failure.Error() == "" {
402+ t.Fatal("Error() must render a message")
403+ }
404+ })
405+ }
406+ if err := (BrokerError{Code: BrokerErrorCode(0)}).Validate(); err == nil {
407+ t.Fatal("zero broker error code accepted")
408+ }
409+ oversize := BrokerError{Code: BrokerErrorTimeout, Text: strings.Repeat("e", BrokerMaxErrorBytes+1)}
410+ if err := oversize.Validate(); err == nil {
411+ t.Fatal("oversize broker error text accepted")
412+ }
413+ unsafe := BrokerError{Code: BrokerErrorTimeout, Text: "timeout\u202erorrE"}
414+ if err := unsafe.Validate(); err == nil {
415+ t.Fatal("bidirectional control in broker error accepted")
416+ }
417+}
418+
419+func TestBrokerIdentityScopesStayDistinct(t *testing.T) {
420+ // Every identity level validates independently and zero values never
421+ // identify anything: connection, stream, operation, daemon incarnation,
422+ // registration, session lifecycle, and broker epoch are distinct types.
423+ if err := (BrokerConnectionID{}).Validate(); err == nil {
424+ t.Fatal("zero connection ID accepted")
425+ }
426+ if err := BrokerStreamID(0).Validate(); err == nil {
427+ t.Fatal("zero stream ID accepted")
428+ }
429+ if err := (BrokerOperationID{}).Validate(); err == nil {
430+ t.Fatal("zero operation ID accepted")
431+ }
432+ if err := (BrokerDaemonIncarnation{}).Validate(); err == nil {
433+ t.Fatal("zero daemon incarnation accepted")
434+ }
435+ if err := (domain.RemoteRegistration{}).Validate(); err == nil {
436+ t.Fatal("zero host registration accepted")
437+ }
438+ if err := (protocol.ExactSessionTarget{}).Validate(); err == nil {
439+ t.Fatal("zero session lifecycle target accepted")
440+ }
441+ if err := BrokerDaemonIdentity("").Validate(); err == nil {
442+ t.Fatal("empty daemon identity accepted")
443+ }
444+ if err := (BrokerStreamLost{}).Validate(); err == nil {
445+ t.Fatal("zero stream loss accepted")
446+ }
447+ loss := BrokerStreamLost{
448+ Connection: BrokerConnectionID{1},
449+ Stream: 2,
450+ Epoch: 3,
451+ Cause: domain.RemoteFailureTimeout,
452+ }
453+ if err := loss.Validate(); err != nil {
454+ t.Fatalf("Validate() = %v", err)
455+ }
456+}
457+
458+func withBrokerPolicy(base BrokerPolicy, mutate func(*BrokerPolicy)) BrokerPolicy {
459+ mutate(&base)
460+ return base
461+}
462+
463+func TestBrokerInterfacesArePorts(t *testing.T) {
464+ // Broker, client, and daemon cross-layer contracts are ports: the
465+ // seams must stay interfaces so Mockery can generate mocks and
466+ // adapters can implement them without importing use cases.
467+ for name, iface := range map[string]reflect.Type{
468+ "BrokerService": reflect.TypeOf((*BrokerService)(nil)).Elem(),
469+ "BrokerSnapshotStore": reflect.TypeOf((*BrokerSnapshotStore)(nil)).Elem(),
470+ "BrokerPhysicalConnection": reflect.TypeOf((*BrokerPhysicalConnection)(nil)).Elem(),
471+ "BrokerEndpointConnector": reflect.TypeOf((*BrokerEndpointConnector)(nil)).Elem(),
472+ "BrokerListener": reflect.TypeOf((*BrokerListener)(nil)).Elem(),
473+ "BrokerSubscription": reflect.TypeOf((*BrokerSubscription)(nil)).Elem(),
474+ } {
475+ if iface.Kind() != reflect.Interface {
476+ t.Fatalf("%s must stay an interface, got %v", name, iface.Kind())
477+ }
478+ }
479+}
A internal/ports/mocks/broker_mock.go +1297 -0
@@ -0,0 +1,1297 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/vev/internal/ports"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockBrokerSubscription creates a new instance of MockBrokerSubscription. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockBrokerSubscription(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockBrokerSubscription {
20+ mock := &MockBrokerSubscription{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockBrokerSubscription is an autogenerated mock type for the BrokerSubscription type
29+type MockBrokerSubscription struct {
30+ mock.Mock
31+}
32+
33+type MockBrokerSubscription_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockBrokerSubscription) EXPECT() *MockBrokerSubscription_Expecter {
38+ return &MockBrokerSubscription_Expecter{mock: &_m.Mock}
39+}
40+
41+// Changed provides a mock function for the type MockBrokerSubscription
42+func (_mock *MockBrokerSubscription) Changed() <-chan struct{} {
43+ ret := _mock.Called()
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for Changed")
47+ }
48+
49+ var r0 <-chan struct{}
50+ if returnFunc, ok := ret.Get(0).(func() <-chan struct{}); ok {
51+ r0 = returnFunc()
52+ } else {
53+ if ret.Get(0) != nil {
54+ r0 = ret.Get(0).(<-chan struct{})
55+ }
56+ }
57+ return r0
58+}
59+
60+// MockBrokerSubscription_Changed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Changed'
61+type MockBrokerSubscription_Changed_Call struct {
62+ *mock.Call
63+}
64+
65+// Changed is a helper method to define mock.On call
66+func (_e *MockBrokerSubscription_Expecter) Changed() *MockBrokerSubscription_Changed_Call {
67+ return &MockBrokerSubscription_Changed_Call{Call: _e.mock.On("Changed")}
68+}
69+
70+func (_c *MockBrokerSubscription_Changed_Call) Run(run func()) *MockBrokerSubscription_Changed_Call {
71+ _c.Call.Run(func(args mock.Arguments) {
72+ run()
73+ })
74+ return _c
75+}
76+
77+func (_c *MockBrokerSubscription_Changed_Call) Return(valCh <-chan struct{}) *MockBrokerSubscription_Changed_Call {
78+ _c.Call.Return(valCh)
79+ return _c
80+}
81+
82+func (_c *MockBrokerSubscription_Changed_Call) RunAndReturn(run func() <-chan struct{}) *MockBrokerSubscription_Changed_Call {
83+ _c.Call.Return(run)
84+ return _c
85+}
86+
87+// Close provides a mock function for the type MockBrokerSubscription
88+func (_mock *MockBrokerSubscription) Close() {
89+ _mock.Called()
90+ return
91+}
92+
93+// MockBrokerSubscription_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
94+type MockBrokerSubscription_Close_Call struct {
95+ *mock.Call
96+}
97+
98+// Close is a helper method to define mock.On call
99+func (_e *MockBrokerSubscription_Expecter) Close() *MockBrokerSubscription_Close_Call {
100+ return &MockBrokerSubscription_Close_Call{Call: _e.mock.On("Close")}
101+}
102+
103+func (_c *MockBrokerSubscription_Close_Call) Run(run func()) *MockBrokerSubscription_Close_Call {
104+ _c.Call.Run(func(args mock.Arguments) {
105+ run()
106+ })
107+ return _c
108+}
109+
110+func (_c *MockBrokerSubscription_Close_Call) Return() *MockBrokerSubscription_Close_Call {
111+ _c.Call.Return()
112+ return _c
113+}
114+
115+func (_c *MockBrokerSubscription_Close_Call) RunAndReturn(run func()) *MockBrokerSubscription_Close_Call {
116+ _c.Run(run)
117+ return _c
118+}
119+
120+// NewMockBrokerService creates a new instance of MockBrokerService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
121+// The first argument is typically a *testing.T value.
122+func NewMockBrokerService(t interface {
123+ mock.TestingT
124+ Cleanup(func())
125+}) *MockBrokerService {
126+ mock := &MockBrokerService{}
127+ mock.Mock.Test(t)
128+
129+ t.Cleanup(func() { mock.AssertExpectations(t) })
130+
131+ return mock
132+}
133+
134+// MockBrokerService is an autogenerated mock type for the BrokerService type
135+type MockBrokerService struct {
136+ mock.Mock
137+}
138+
139+type MockBrokerService_Expecter struct {
140+ mock *mock.Mock
141+}
142+
143+func (_m *MockBrokerService) EXPECT() *MockBrokerService_Expecter {
144+ return &MockBrokerService_Expecter{mock: &_m.Mock}
145+}
146+
147+// AddHost provides a mock function for the type MockBrokerService
148+func (_mock *MockBrokerService) AddHost(ctx context.Context, target string) error {
149+ ret := _mock.Called(ctx, target)
150+
151+ if len(ret) == 0 {
152+ panic("no return value specified for AddHost")
153+ }
154+
155+ var r0 error
156+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
157+ r0 = returnFunc(ctx, target)
158+ } else {
159+ r0 = ret.Error(0)
160+ }
161+ return r0
162+}
163+
164+// MockBrokerService_AddHost_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddHost'
165+type MockBrokerService_AddHost_Call struct {
166+ *mock.Call
167+}
168+
169+// AddHost is a helper method to define mock.On call
170+// - ctx context.Context
171+// - target string
172+func (_e *MockBrokerService_Expecter) AddHost(ctx any, target any) *MockBrokerService_AddHost_Call {
173+ return &MockBrokerService_AddHost_Call{Call: _e.mock.On("AddHost", ctx, target)}
174+}
175+
176+func (_c *MockBrokerService_AddHost_Call) Run(run func(ctx context.Context, target string)) *MockBrokerService_AddHost_Call {
177+ _c.Call.Run(func(args mock.Arguments) {
178+ var arg0 context.Context
179+ if args[0] != nil {
180+ arg0 = args[0].(context.Context)
181+ }
182+ var arg1 string
183+ if args[1] != nil {
184+ arg1 = args[1].(string)
185+ }
186+ run(
187+ arg0,
188+ arg1,
189+ )
190+ })
191+ return _c
192+}
193+
194+func (_c *MockBrokerService_AddHost_Call) Return(err error) *MockBrokerService_AddHost_Call {
195+ _c.Call.Return(err)
196+ return _c
197+}
198+
199+func (_c *MockBrokerService_AddHost_Call) RunAndReturn(run func(ctx context.Context, target string) error) *MockBrokerService_AddHost_Call {
200+ _c.Call.Return(run)
201+ return _c
202+}
203+
204+// Close provides a mock function for the type MockBrokerService
205+func (_mock *MockBrokerService) Close() error {
206+ ret := _mock.Called()
207+
208+ if len(ret) == 0 {
209+ panic("no return value specified for Close")
210+ }
211+
212+ var r0 error
213+ if returnFunc, ok := ret.Get(0).(func() error); ok {
214+ r0 = returnFunc()
215+ } else {
216+ r0 = ret.Error(0)
217+ }
218+ return r0
219+}
220+
221+// MockBrokerService_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
222+type MockBrokerService_Close_Call struct {
223+ *mock.Call
224+}
225+
226+// Close is a helper method to define mock.On call
227+func (_e *MockBrokerService_Expecter) Close() *MockBrokerService_Close_Call {
228+ return &MockBrokerService_Close_Call{Call: _e.mock.On("Close")}
229+}
230+
231+func (_c *MockBrokerService_Close_Call) Run(run func()) *MockBrokerService_Close_Call {
232+ _c.Call.Run(func(args mock.Arguments) {
233+ run()
234+ })
235+ return _c
236+}
237+
238+func (_c *MockBrokerService_Close_Call) Return(err error) *MockBrokerService_Close_Call {
239+ _c.Call.Return(err)
240+ return _c
241+}
242+
243+func (_c *MockBrokerService_Close_Call) RunAndReturn(run func() error) *MockBrokerService_Close_Call {
244+ _c.Call.Return(run)
245+ return _c
246+}
247+
248+// CloseStream provides a mock function for the type MockBrokerService
249+func (_mock *MockBrokerService) CloseStream(connection ports.BrokerConnectionID, stream ports.BrokerStreamID) error {
250+ ret := _mock.Called(connection, stream)
251+
252+ if len(ret) == 0 {
253+ panic("no return value specified for CloseStream")
254+ }
255+
256+ var r0 error
257+ if returnFunc, ok := ret.Get(0).(func(ports.BrokerConnectionID, ports.BrokerStreamID) error); ok {
258+ r0 = returnFunc(connection, stream)
259+ } else {
260+ r0 = ret.Error(0)
261+ }
262+ return r0
263+}
264+
265+// MockBrokerService_CloseStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseStream'
266+type MockBrokerService_CloseStream_Call struct {
267+ *mock.Call
268+}
269+
270+// CloseStream is a helper method to define mock.On call
271+// - connection ports.BrokerConnectionID
272+// - stream ports.BrokerStreamID
273+func (_e *MockBrokerService_Expecter) CloseStream(connection any, stream any) *MockBrokerService_CloseStream_Call {
274+ return &MockBrokerService_CloseStream_Call{Call: _e.mock.On("CloseStream", connection, stream)}
275+}
276+
277+func (_c *MockBrokerService_CloseStream_Call) Run(run func(connection ports.BrokerConnectionID, stream ports.BrokerStreamID)) *MockBrokerService_CloseStream_Call {
278+ _c.Call.Run(func(args mock.Arguments) {
279+ var arg0 ports.BrokerConnectionID
280+ if args[0] != nil {
281+ arg0 = args[0].(ports.BrokerConnectionID)
282+ }
283+ var arg1 ports.BrokerStreamID
284+ if args[1] != nil {
285+ arg1 = args[1].(ports.BrokerStreamID)
286+ }
287+ run(
288+ arg0,
289+ arg1,
290+ )
291+ })
292+ return _c
293+}
294+
295+func (_c *MockBrokerService_CloseStream_Call) Return(err error) *MockBrokerService_CloseStream_Call {
296+ _c.Call.Return(err)
297+ return _c
298+}
299+
300+func (_c *MockBrokerService_CloseStream_Call) RunAndReturn(run func(connection ports.BrokerConnectionID, stream ports.BrokerStreamID) error) *MockBrokerService_CloseStream_Call {
301+ _c.Call.Return(run)
302+ return _c
303+}
304+
305+// ConnectionID provides a mock function for the type MockBrokerService
306+func (_mock *MockBrokerService) ConnectionID() ports.BrokerConnectionID {
307+ ret := _mock.Called()
308+
309+ if len(ret) == 0 {
310+ panic("no return value specified for ConnectionID")
311+ }
312+
313+ var r0 ports.BrokerConnectionID
314+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerConnectionID); ok {
315+ r0 = returnFunc()
316+ } else {
317+ if ret.Get(0) != nil {
318+ r0 = ret.Get(0).(ports.BrokerConnectionID)
319+ }
320+ }
321+ return r0
322+}
323+
324+// MockBrokerService_ConnectionID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConnectionID'
325+type MockBrokerService_ConnectionID_Call struct {
326+ *mock.Call
327+}
328+
329+// ConnectionID is a helper method to define mock.On call
330+func (_e *MockBrokerService_Expecter) ConnectionID() *MockBrokerService_ConnectionID_Call {
331+ return &MockBrokerService_ConnectionID_Call{Call: _e.mock.On("ConnectionID")}
332+}
333+
334+func (_c *MockBrokerService_ConnectionID_Call) Run(run func()) *MockBrokerService_ConnectionID_Call {
335+ _c.Call.Run(func(args mock.Arguments) {
336+ run()
337+ })
338+ return _c
339+}
340+
341+func (_c *MockBrokerService_ConnectionID_Call) Return(brokerConnectionID ports.BrokerConnectionID) *MockBrokerService_ConnectionID_Call {
342+ _c.Call.Return(brokerConnectionID)
343+ return _c
344+}
345+
346+func (_c *MockBrokerService_ConnectionID_Call) RunAndReturn(run func() ports.BrokerConnectionID) *MockBrokerService_ConnectionID_Call {
347+ _c.Call.Return(run)
348+ return _c
349+}
350+
351+// OpenStream provides a mock function for the type MockBrokerService
352+func (_mock *MockBrokerService) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error) {
353+ ret := _mock.Called(ctx, request)
354+
355+ if len(ret) == 0 {
356+ panic("no return value specified for OpenStream")
357+ }
358+
359+ var r0 ports.ClientConnection
360+ var r1 error
361+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)); ok {
362+ return returnFunc(ctx, request)
363+ }
364+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.ClientConnection); ok {
365+ r0 = returnFunc(ctx, request)
366+ } else {
367+ if ret.Get(0) != nil {
368+ r0 = ret.Get(0).(ports.ClientConnection)
369+ }
370+ }
371+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
372+ r1 = returnFunc(ctx, request)
373+ } else {
374+ r1 = ret.Error(1)
375+ }
376+ return r0, r1
377+}
378+
379+// MockBrokerService_OpenStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenStream'
380+type MockBrokerService_OpenStream_Call struct {
381+ *mock.Call
382+}
383+
384+// OpenStream is a helper method to define mock.On call
385+// - ctx context.Context
386+// - request ports.BrokerOpenStreamRequest
387+func (_e *MockBrokerService_Expecter) OpenStream(ctx any, request any) *MockBrokerService_OpenStream_Call {
388+ return &MockBrokerService_OpenStream_Call{Call: _e.mock.On("OpenStream", ctx, request)}
389+}
390+
391+func (_c *MockBrokerService_OpenStream_Call) Run(run func(ctx context.Context, request ports.BrokerOpenStreamRequest)) *MockBrokerService_OpenStream_Call {
392+ _c.Call.Run(func(args mock.Arguments) {
393+ var arg0 context.Context
394+ if args[0] != nil {
395+ arg0 = args[0].(context.Context)
396+ }
397+ var arg1 ports.BrokerOpenStreamRequest
398+ if args[1] != nil {
399+ arg1 = args[1].(ports.BrokerOpenStreamRequest)
400+ }
401+ run(
402+ arg0,
403+ arg1,
404+ )
405+ })
406+ return _c
407+}
408+
409+func (_c *MockBrokerService_OpenStream_Call) Return(clientConnection ports.ClientConnection, err error) *MockBrokerService_OpenStream_Call {
410+ _c.Call.Return(clientConnection, err)
411+ return _c
412+}
413+
414+func (_c *MockBrokerService_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)) *MockBrokerService_OpenStream_Call {
415+ _c.Call.Return(run)
416+ return _c
417+}
418+
419+// RemoveHost provides a mock function for the type MockBrokerService
420+func (_mock *MockBrokerService) RemoveHost(ctx context.Context, target string) (bool, error) {
421+ ret := _mock.Called(ctx, target)
422+
423+ if len(ret) == 0 {
424+ panic("no return value specified for RemoveHost")
425+ }
426+
427+ var r0 bool
428+ var r1 error
429+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
430+ return returnFunc(ctx, target)
431+ }
432+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok {
433+ r0 = returnFunc(ctx, target)
434+ } else {
435+ r0 = ret.Get(0).(bool)
436+ }
437+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
438+ r1 = returnFunc(ctx, target)
439+ } else {
440+ r1 = ret.Error(1)
441+ }
442+ return r0, r1
443+}
444+
445+// MockBrokerService_RemoveHost_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveHost'
446+type MockBrokerService_RemoveHost_Call struct {
447+ *mock.Call
448+}
449+
450+// RemoveHost is a helper method to define mock.On call
451+// - ctx context.Context
452+// - target string
453+func (_e *MockBrokerService_Expecter) RemoveHost(ctx any, target any) *MockBrokerService_RemoveHost_Call {
454+ return &MockBrokerService_RemoveHost_Call{Call: _e.mock.On("RemoveHost", ctx, target)}
455+}
456+
457+func (_c *MockBrokerService_RemoveHost_Call) Run(run func(ctx context.Context, target string)) *MockBrokerService_RemoveHost_Call {
458+ _c.Call.Run(func(args mock.Arguments) {
459+ var arg0 context.Context
460+ if args[0] != nil {
461+ arg0 = args[0].(context.Context)
462+ }
463+ var arg1 string
464+ if args[1] != nil {
465+ arg1 = args[1].(string)
466+ }
467+ run(
468+ arg0,
469+ arg1,
470+ )
471+ })
472+ return _c
473+}
474+
475+func (_c *MockBrokerService_RemoveHost_Call) Return(b bool, err error) *MockBrokerService_RemoveHost_Call {
476+ _c.Call.Return(b, err)
477+ return _c
478+}
479+
480+func (_c *MockBrokerService_RemoveHost_Call) RunAndReturn(run func(ctx context.Context, target string) (bool, error)) *MockBrokerService_RemoveHost_Call {
481+ _c.Call.Return(run)
482+ return _c
483+}
484+
485+// RequestReconcile provides a mock function for the type MockBrokerService
486+func (_mock *MockBrokerService) RequestReconcile(endpoint string) {
487+ _mock.Called(endpoint)
488+ return
489+}
490+
491+// MockBrokerService_RequestReconcile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequestReconcile'
492+type MockBrokerService_RequestReconcile_Call struct {
493+ *mock.Call
494+}
495+
496+// RequestReconcile is a helper method to define mock.On call
497+// - endpoint string
498+func (_e *MockBrokerService_Expecter) RequestReconcile(endpoint any) *MockBrokerService_RequestReconcile_Call {
499+ return &MockBrokerService_RequestReconcile_Call{Call: _e.mock.On("RequestReconcile", endpoint)}
500+}
501+
502+func (_c *MockBrokerService_RequestReconcile_Call) Run(run func(endpoint string)) *MockBrokerService_RequestReconcile_Call {
503+ _c.Call.Run(func(args mock.Arguments) {
504+ var arg0 string
505+ if args[0] != nil {
506+ arg0 = args[0].(string)
507+ }
508+ run(
509+ arg0,
510+ )
511+ })
512+ return _c
513+}
514+
515+func (_c *MockBrokerService_RequestReconcile_Call) Return() *MockBrokerService_RequestReconcile_Call {
516+ _c.Call.Return()
517+ return _c
518+}
519+
520+func (_c *MockBrokerService_RequestReconcile_Call) RunAndReturn(run func(endpoint string)) *MockBrokerService_RequestReconcile_Call {
521+ _c.Run(run)
522+ return _c
523+}
524+
525+// Snapshot provides a mock function for the type MockBrokerService
526+func (_mock *MockBrokerService) Snapshot() ports.BrokerSnapshot {
527+ ret := _mock.Called()
528+
529+ if len(ret) == 0 {
530+ panic("no return value specified for Snapshot")
531+ }
532+
533+ var r0 ports.BrokerSnapshot
534+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerSnapshot); ok {
535+ r0 = returnFunc()
536+ } else {
537+ r0 = ret.Get(0).(ports.BrokerSnapshot)
538+ }
539+ return r0
540+}
541+
542+// MockBrokerService_Snapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Snapshot'
543+type MockBrokerService_Snapshot_Call struct {
544+ *mock.Call
545+}
546+
547+// Snapshot is a helper method to define mock.On call
548+func (_e *MockBrokerService_Expecter) Snapshot() *MockBrokerService_Snapshot_Call {
549+ return &MockBrokerService_Snapshot_Call{Call: _e.mock.On("Snapshot")}
550+}
551+
552+func (_c *MockBrokerService_Snapshot_Call) Run(run func()) *MockBrokerService_Snapshot_Call {
553+ _c.Call.Run(func(args mock.Arguments) {
554+ run()
555+ })
556+ return _c
557+}
558+
559+func (_c *MockBrokerService_Snapshot_Call) Return(brokerSnapshot ports.BrokerSnapshot) *MockBrokerService_Snapshot_Call {
560+ _c.Call.Return(brokerSnapshot)
561+ return _c
562+}
563+
564+func (_c *MockBrokerService_Snapshot_Call) RunAndReturn(run func() ports.BrokerSnapshot) *MockBrokerService_Snapshot_Call {
565+ _c.Call.Return(run)
566+ return _c
567+}
568+
569+// Subscribe provides a mock function for the type MockBrokerService
570+func (_mock *MockBrokerService) Subscribe() (ports.BrokerSubscription, error) {
571+ ret := _mock.Called()
572+
573+ if len(ret) == 0 {
574+ panic("no return value specified for Subscribe")
575+ }
576+
577+ var r0 ports.BrokerSubscription
578+ var r1 error
579+ if returnFunc, ok := ret.Get(0).(func() (ports.BrokerSubscription, error)); ok {
580+ return returnFunc()
581+ }
582+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerSubscription); ok {
583+ r0 = returnFunc()
584+ } else {
585+ if ret.Get(0) != nil {
586+ r0 = ret.Get(0).(ports.BrokerSubscription)
587+ }
588+ }
589+ if returnFunc, ok := ret.Get(1).(func() error); ok {
590+ r1 = returnFunc()
591+ } else {
592+ r1 = ret.Error(1)
593+ }
594+ return r0, r1
595+}
596+
597+// MockBrokerService_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
598+type MockBrokerService_Subscribe_Call struct {
599+ *mock.Call
600+}
601+
602+// Subscribe is a helper method to define mock.On call
603+func (_e *MockBrokerService_Expecter) Subscribe() *MockBrokerService_Subscribe_Call {
604+ return &MockBrokerService_Subscribe_Call{Call: _e.mock.On("Subscribe")}
605+}
606+
607+func (_c *MockBrokerService_Subscribe_Call) Run(run func()) *MockBrokerService_Subscribe_Call {
608+ _c.Call.Run(func(args mock.Arguments) {
609+ run()
610+ })
611+ return _c
612+}
613+
614+func (_c *MockBrokerService_Subscribe_Call) Return(brokerSubscription ports.BrokerSubscription, err error) *MockBrokerService_Subscribe_Call {
615+ _c.Call.Return(brokerSubscription, err)
616+ return _c
617+}
618+
619+func (_c *MockBrokerService_Subscribe_Call) RunAndReturn(run func() (ports.BrokerSubscription, error)) *MockBrokerService_Subscribe_Call {
620+ _c.Call.Return(run)
621+ return _c
622+}
623+
624+// NewMockBrokerSnapshotStore creates a new instance of MockBrokerSnapshotStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
625+// The first argument is typically a *testing.T value.
626+func NewMockBrokerSnapshotStore(t interface {
627+ mock.TestingT
628+ Cleanup(func())
629+}) *MockBrokerSnapshotStore {
630+ mock := &MockBrokerSnapshotStore{}
631+ mock.Mock.Test(t)
632+
633+ t.Cleanup(func() { mock.AssertExpectations(t) })
634+
635+ return mock
636+}
637+
638+// MockBrokerSnapshotStore is an autogenerated mock type for the BrokerSnapshotStore type
639+type MockBrokerSnapshotStore struct {
640+ mock.Mock
641+}
642+
643+type MockBrokerSnapshotStore_Expecter struct {
644+ mock *mock.Mock
645+}
646+
647+func (_m *MockBrokerSnapshotStore) EXPECT() *MockBrokerSnapshotStore_Expecter {
648+ return &MockBrokerSnapshotStore_Expecter{mock: &_m.Mock}
649+}
650+
651+// Load provides a mock function for the type MockBrokerSnapshotStore
652+func (_mock *MockBrokerSnapshotStore) Load() (ports.BrokerSnapshot, error) {
653+ ret := _mock.Called()
654+
655+ if len(ret) == 0 {
656+ panic("no return value specified for Load")
657+ }
658+
659+ var r0 ports.BrokerSnapshot
660+ var r1 error
661+ if returnFunc, ok := ret.Get(0).(func() (ports.BrokerSnapshot, error)); ok {
662+ return returnFunc()
663+ }
664+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerSnapshot); ok {
665+ r0 = returnFunc()
666+ } else {
667+ r0 = ret.Get(0).(ports.BrokerSnapshot)
668+ }
669+ if returnFunc, ok := ret.Get(1).(func() error); ok {
670+ r1 = returnFunc()
671+ } else {
672+ r1 = ret.Error(1)
673+ }
674+ return r0, r1
675+}
676+
677+// MockBrokerSnapshotStore_Load_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Load'
678+type MockBrokerSnapshotStore_Load_Call struct {
679+ *mock.Call
680+}
681+
682+// Load is a helper method to define mock.On call
683+func (_e *MockBrokerSnapshotStore_Expecter) Load() *MockBrokerSnapshotStore_Load_Call {
684+ return &MockBrokerSnapshotStore_Load_Call{Call: _e.mock.On("Load")}
685+}
686+
687+func (_c *MockBrokerSnapshotStore_Load_Call) Run(run func()) *MockBrokerSnapshotStore_Load_Call {
688+ _c.Call.Run(func(args mock.Arguments) {
689+ run()
690+ })
691+ return _c
692+}
693+
694+func (_c *MockBrokerSnapshotStore_Load_Call) Return(brokerSnapshot ports.BrokerSnapshot, err error) *MockBrokerSnapshotStore_Load_Call {
695+ _c.Call.Return(brokerSnapshot, err)
696+ return _c
697+}
698+
699+func (_c *MockBrokerSnapshotStore_Load_Call) RunAndReturn(run func() (ports.BrokerSnapshot, error)) *MockBrokerSnapshotStore_Load_Call {
700+ _c.Call.Return(run)
701+ return _c
702+}
703+
704+// Store provides a mock function for the type MockBrokerSnapshotStore
705+func (_mock *MockBrokerSnapshotStore) Store(brokerSnapshot ports.BrokerSnapshot) error {
706+ ret := _mock.Called(brokerSnapshot)
707+
708+ if len(ret) == 0 {
709+ panic("no return value specified for Store")
710+ }
711+
712+ var r0 error
713+ if returnFunc, ok := ret.Get(0).(func(ports.BrokerSnapshot) error); ok {
714+ r0 = returnFunc(brokerSnapshot)
715+ } else {
716+ r0 = ret.Error(0)
717+ }
718+ return r0
719+}
720+
721+// MockBrokerSnapshotStore_Store_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Store'
722+type MockBrokerSnapshotStore_Store_Call struct {
723+ *mock.Call
724+}
725+
726+// Store is a helper method to define mock.On call
727+// - brokerSnapshot ports.BrokerSnapshot
728+func (_e *MockBrokerSnapshotStore_Expecter) Store(brokerSnapshot any) *MockBrokerSnapshotStore_Store_Call {
729+ return &MockBrokerSnapshotStore_Store_Call{Call: _e.mock.On("Store", brokerSnapshot)}
730+}
731+
732+func (_c *MockBrokerSnapshotStore_Store_Call) Run(run func(brokerSnapshot ports.BrokerSnapshot)) *MockBrokerSnapshotStore_Store_Call {
733+ _c.Call.Run(func(args mock.Arguments) {
734+ var arg0 ports.BrokerSnapshot
735+ if args[0] != nil {
736+ arg0 = args[0].(ports.BrokerSnapshot)
737+ }
738+ run(
739+ arg0,
740+ )
741+ })
742+ return _c
743+}
744+
745+func (_c *MockBrokerSnapshotStore_Store_Call) Return(err error) *MockBrokerSnapshotStore_Store_Call {
746+ _c.Call.Return(err)
747+ return _c
748+}
749+
750+func (_c *MockBrokerSnapshotStore_Store_Call) RunAndReturn(run func(brokerSnapshot ports.BrokerSnapshot) error) *MockBrokerSnapshotStore_Store_Call {
751+ _c.Call.Return(run)
752+ return _c
753+}
754+
755+// NewMockBrokerPhysicalConnection creates a new instance of MockBrokerPhysicalConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
756+// The first argument is typically a *testing.T value.
757+func NewMockBrokerPhysicalConnection(t interface {
758+ mock.TestingT
759+ Cleanup(func())
760+}) *MockBrokerPhysicalConnection {
761+ mock := &MockBrokerPhysicalConnection{}
762+ mock.Mock.Test(t)
763+
764+ t.Cleanup(func() { mock.AssertExpectations(t) })
765+
766+ return mock
767+}
768+
769+// MockBrokerPhysicalConnection is an autogenerated mock type for the BrokerPhysicalConnection type
770+type MockBrokerPhysicalConnection struct {
771+ mock.Mock
772+}
773+
774+type MockBrokerPhysicalConnection_Expecter struct {
775+ mock *mock.Mock
776+}
777+
778+func (_m *MockBrokerPhysicalConnection) EXPECT() *MockBrokerPhysicalConnection_Expecter {
779+ return &MockBrokerPhysicalConnection_Expecter{mock: &_m.Mock}
780+}
781+
782+// Close provides a mock function for the type MockBrokerPhysicalConnection
783+func (_mock *MockBrokerPhysicalConnection) Close() error {
784+ ret := _mock.Called()
785+
786+ if len(ret) == 0 {
787+ panic("no return value specified for Close")
788+ }
789+
790+ var r0 error
791+ if returnFunc, ok := ret.Get(0).(func() error); ok {
792+ r0 = returnFunc()
793+ } else {
794+ r0 = ret.Error(0)
795+ }
796+ return r0
797+}
798+
799+// MockBrokerPhysicalConnection_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
800+type MockBrokerPhysicalConnection_Close_Call struct {
801+ *mock.Call
802+}
803+
804+// Close is a helper method to define mock.On call
805+func (_e *MockBrokerPhysicalConnection_Expecter) Close() *MockBrokerPhysicalConnection_Close_Call {
806+ return &MockBrokerPhysicalConnection_Close_Call{Call: _e.mock.On("Close")}
807+}
808+
809+func (_c *MockBrokerPhysicalConnection_Close_Call) Run(run func()) *MockBrokerPhysicalConnection_Close_Call {
810+ _c.Call.Run(func(args mock.Arguments) {
811+ run()
812+ })
813+ return _c
814+}
815+
816+func (_c *MockBrokerPhysicalConnection_Close_Call) Return(err error) *MockBrokerPhysicalConnection_Close_Call {
817+ _c.Call.Return(err)
818+ return _c
819+}
820+
821+func (_c *MockBrokerPhysicalConnection_Close_Call) RunAndReturn(run func() error) *MockBrokerPhysicalConnection_Close_Call {
822+ _c.Call.Return(run)
823+ return _c
824+}
825+
826+// Identity provides a mock function for the type MockBrokerPhysicalConnection
827+func (_mock *MockBrokerPhysicalConnection) Identity() ports.BrokerDaemonIdentity {
828+ ret := _mock.Called()
829+
830+ if len(ret) == 0 {
831+ panic("no return value specified for Identity")
832+ }
833+
834+ var r0 ports.BrokerDaemonIdentity
835+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerDaemonIdentity); ok {
836+ r0 = returnFunc()
837+ } else {
838+ r0 = ret.Get(0).(ports.BrokerDaemonIdentity)
839+ }
840+ return r0
841+}
842+
843+// MockBrokerPhysicalConnection_Identity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Identity'
844+type MockBrokerPhysicalConnection_Identity_Call struct {
845+ *mock.Call
846+}
847+
848+// Identity is a helper method to define mock.On call
849+func (_e *MockBrokerPhysicalConnection_Expecter) Identity() *MockBrokerPhysicalConnection_Identity_Call {
850+ return &MockBrokerPhysicalConnection_Identity_Call{Call: _e.mock.On("Identity")}
851+}
852+
853+func (_c *MockBrokerPhysicalConnection_Identity_Call) Run(run func()) *MockBrokerPhysicalConnection_Identity_Call {
854+ _c.Call.Run(func(args mock.Arguments) {
855+ run()
856+ })
857+ return _c
858+}
859+
860+func (_c *MockBrokerPhysicalConnection_Identity_Call) Return(brokerDaemonIdentity ports.BrokerDaemonIdentity) *MockBrokerPhysicalConnection_Identity_Call {
861+ _c.Call.Return(brokerDaemonIdentity)
862+ return _c
863+}
864+
865+func (_c *MockBrokerPhysicalConnection_Identity_Call) RunAndReturn(run func() ports.BrokerDaemonIdentity) *MockBrokerPhysicalConnection_Identity_Call {
866+ _c.Call.Return(run)
867+ return _c
868+}
869+
870+// Incarnation provides a mock function for the type MockBrokerPhysicalConnection
871+func (_mock *MockBrokerPhysicalConnection) Incarnation() ports.BrokerDaemonIncarnation {
872+ ret := _mock.Called()
873+
874+ if len(ret) == 0 {
875+ panic("no return value specified for Incarnation")
876+ }
877+
878+ var r0 ports.BrokerDaemonIncarnation
879+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerDaemonIncarnation); ok {
880+ r0 = returnFunc()
881+ } else {
882+ if ret.Get(0) != nil {
883+ r0 = ret.Get(0).(ports.BrokerDaemonIncarnation)
884+ }
885+ }
886+ return r0
887+}
888+
889+// MockBrokerPhysicalConnection_Incarnation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Incarnation'
890+type MockBrokerPhysicalConnection_Incarnation_Call struct {
891+ *mock.Call
892+}
893+
894+// Incarnation is a helper method to define mock.On call
895+func (_e *MockBrokerPhysicalConnection_Expecter) Incarnation() *MockBrokerPhysicalConnection_Incarnation_Call {
896+ return &MockBrokerPhysicalConnection_Incarnation_Call{Call: _e.mock.On("Incarnation")}
897+}
898+
899+func (_c *MockBrokerPhysicalConnection_Incarnation_Call) Run(run func()) *MockBrokerPhysicalConnection_Incarnation_Call {
900+ _c.Call.Run(func(args mock.Arguments) {
901+ run()
902+ })
903+ return _c
904+}
905+
906+func (_c *MockBrokerPhysicalConnection_Incarnation_Call) Return(brokerDaemonIncarnation ports.BrokerDaemonIncarnation) *MockBrokerPhysicalConnection_Incarnation_Call {
907+ _c.Call.Return(brokerDaemonIncarnation)
908+ return _c
909+}
910+
911+func (_c *MockBrokerPhysicalConnection_Incarnation_Call) RunAndReturn(run func() ports.BrokerDaemonIncarnation) *MockBrokerPhysicalConnection_Incarnation_Call {
912+ _c.Call.Return(run)
913+ return _c
914+}
915+
916+// OpenStream provides a mock function for the type MockBrokerPhysicalConnection
917+func (_mock *MockBrokerPhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error) {
918+ ret := _mock.Called(ctx, request)
919+
920+ if len(ret) == 0 {
921+ panic("no return value specified for OpenStream")
922+ }
923+
924+ var r0 ports.ClientConnection
925+ var r1 error
926+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)); ok {
927+ return returnFunc(ctx, request)
928+ }
929+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.ClientConnection); ok {
930+ r0 = returnFunc(ctx, request)
931+ } else {
932+ if ret.Get(0) != nil {
933+ r0 = ret.Get(0).(ports.ClientConnection)
934+ }
935+ }
936+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
937+ r1 = returnFunc(ctx, request)
938+ } else {
939+ r1 = ret.Error(1)
940+ }
941+ return r0, r1
942+}
943+
944+// MockBrokerPhysicalConnection_OpenStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenStream'
945+type MockBrokerPhysicalConnection_OpenStream_Call struct {
946+ *mock.Call
947+}
948+
949+// OpenStream is a helper method to define mock.On call
950+// - ctx context.Context
951+// - request ports.BrokerOpenStreamRequest
952+func (_e *MockBrokerPhysicalConnection_Expecter) OpenStream(ctx any, request any) *MockBrokerPhysicalConnection_OpenStream_Call {
953+ return &MockBrokerPhysicalConnection_OpenStream_Call{Call: _e.mock.On("OpenStream", ctx, request)}
954+}
955+
956+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Run(run func(ctx context.Context, request ports.BrokerOpenStreamRequest)) *MockBrokerPhysicalConnection_OpenStream_Call {
957+ _c.Call.Run(func(args mock.Arguments) {
958+ var arg0 context.Context
959+ if args[0] != nil {
960+ arg0 = args[0].(context.Context)
961+ }
962+ var arg1 ports.BrokerOpenStreamRequest
963+ if args[1] != nil {
964+ arg1 = args[1].(ports.BrokerOpenStreamRequest)
965+ }
966+ run(
967+ arg0,
968+ arg1,
969+ )
970+ })
971+ return _c
972+}
973+
974+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Return(clientConnection ports.ClientConnection, err error) *MockBrokerPhysicalConnection_OpenStream_Call {
975+ _c.Call.Return(clientConnection, err)
976+ return _c
977+}
978+
979+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)) *MockBrokerPhysicalConnection_OpenStream_Call {
980+ _c.Call.Return(run)
981+ return _c
982+}
983+
984+// Policy provides a mock function for the type MockBrokerPhysicalConnection
985+func (_mock *MockBrokerPhysicalConnection) Policy() ports.BrokerPolicy {
986+ ret := _mock.Called()
987+
988+ if len(ret) == 0 {
989+ panic("no return value specified for Policy")
990+ }
991+
992+ var r0 ports.BrokerPolicy
993+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerPolicy); ok {
994+ r0 = returnFunc()
995+ } else {
996+ r0 = ret.Get(0).(ports.BrokerPolicy)
997+ }
998+ return r0
999+}
1000+
1001+// MockBrokerPhysicalConnection_Policy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Policy'
1002+type MockBrokerPhysicalConnection_Policy_Call struct {
1003+ *mock.Call
1004+}
1005+
1006+// Policy is a helper method to define mock.On call
1007+func (_e *MockBrokerPhysicalConnection_Expecter) Policy() *MockBrokerPhysicalConnection_Policy_Call {
1008+ return &MockBrokerPhysicalConnection_Policy_Call{Call: _e.mock.On("Policy")}
1009+}
1010+
1011+func (_c *MockBrokerPhysicalConnection_Policy_Call) Run(run func()) *MockBrokerPhysicalConnection_Policy_Call {
1012+ _c.Call.Run(func(args mock.Arguments) {
1013+ run()
1014+ })
1015+ return _c
1016+}
1017+
1018+func (_c *MockBrokerPhysicalConnection_Policy_Call) Return(brokerPolicy ports.BrokerPolicy) *MockBrokerPhysicalConnection_Policy_Call {
1019+ _c.Call.Return(brokerPolicy)
1020+ return _c
1021+}
1022+
1023+func (_c *MockBrokerPhysicalConnection_Policy_Call) RunAndReturn(run func() ports.BrokerPolicy) *MockBrokerPhysicalConnection_Policy_Call {
1024+ _c.Call.Return(run)
1025+ return _c
1026+}
1027+
1028+// NewMockBrokerEndpointConnector creates a new instance of MockBrokerEndpointConnector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
1029+// The first argument is typically a *testing.T value.
1030+func NewMockBrokerEndpointConnector(t interface {
1031+ mock.TestingT
1032+ Cleanup(func())
1033+}) *MockBrokerEndpointConnector {
1034+ mock := &MockBrokerEndpointConnector{}
1035+ mock.Mock.Test(t)
1036+
1037+ t.Cleanup(func() { mock.AssertExpectations(t) })
1038+
1039+ return mock
1040+}
1041+
1042+// MockBrokerEndpointConnector is an autogenerated mock type for the BrokerEndpointConnector type
1043+type MockBrokerEndpointConnector struct {
1044+ mock.Mock
1045+}
1046+
1047+type MockBrokerEndpointConnector_Expecter struct {
1048+ mock *mock.Mock
1049+}
1050+
1051+func (_m *MockBrokerEndpointConnector) EXPECT() *MockBrokerEndpointConnector_Expecter {
1052+ return &MockBrokerEndpointConnector_Expecter{mock: &_m.Mock}
1053+}
1054+
1055+// Connect provides a mock function for the type MockBrokerEndpointConnector
1056+func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint string, policy ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error) {
1057+ ret := _mock.Called(ctx, endpoint, policy)
1058+
1059+ if len(ret) == 0 {
1060+ panic("no return value specified for Connect")
1061+ }
1062+
1063+ var r0 ports.BrokerPhysicalConnection
1064+ var r1 error
1065+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error)); ok {
1066+ return returnFunc(ctx, endpoint, policy)
1067+ }
1068+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.BrokerPolicy) ports.BrokerPhysicalConnection); ok {
1069+ r0 = returnFunc(ctx, endpoint, policy)
1070+ } else {
1071+ if ret.Get(0) != nil {
1072+ r0 = ret.Get(0).(ports.BrokerPhysicalConnection)
1073+ }
1074+ }
1075+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, ports.BrokerPolicy) error); ok {
1076+ r1 = returnFunc(ctx, endpoint, policy)
1077+ } else {
1078+ r1 = ret.Error(1)
1079+ }
1080+ return r0, r1
1081+}
1082+
1083+// MockBrokerEndpointConnector_Connect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Connect'
1084+type MockBrokerEndpointConnector_Connect_Call struct {
1085+ *mock.Call
1086+}
1087+
1088+// Connect is a helper method to define mock.On call
1089+// - ctx context.Context
1090+// - endpoint string
1091+// - policy ports.BrokerPolicy
1092+func (_e *MockBrokerEndpointConnector_Expecter) Connect(ctx any, endpoint any, policy any) *MockBrokerEndpointConnector_Connect_Call {
1093+ return &MockBrokerEndpointConnector_Connect_Call{Call: _e.mock.On("Connect", ctx, endpoint, policy)}
1094+}
1095+
1096+func (_c *MockBrokerEndpointConnector_Connect_Call) Run(run func(ctx context.Context, endpoint string, policy ports.BrokerPolicy)) *MockBrokerEndpointConnector_Connect_Call {
1097+ _c.Call.Run(func(args mock.Arguments) {
1098+ var arg0 context.Context
1099+ if args[0] != nil {
1100+ arg0 = args[0].(context.Context)
1101+ }
1102+ var arg1 string
1103+ if args[1] != nil {
1104+ arg1 = args[1].(string)
1105+ }
1106+ var arg2 ports.BrokerPolicy
1107+ if args[2] != nil {
1108+ arg2 = args[2].(ports.BrokerPolicy)
1109+ }
1110+ run(
1111+ arg0,
1112+ arg1,
1113+ arg2,
1114+ )
1115+ })
1116+ return _c
1117+}
1118+
1119+func (_c *MockBrokerEndpointConnector_Connect_Call) Return(brokerPhysicalConnection ports.BrokerPhysicalConnection, err error) *MockBrokerEndpointConnector_Connect_Call {
1120+ _c.Call.Return(brokerPhysicalConnection, err)
1121+ return _c
1122+}
1123+
1124+func (_c *MockBrokerEndpointConnector_Connect_Call) RunAndReturn(run func(ctx context.Context, endpoint string, policy ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error)) *MockBrokerEndpointConnector_Connect_Call {
1125+ _c.Call.Return(run)
1126+ return _c
1127+}
1128+
1129+// NewMockBrokerListener creates a new instance of MockBrokerListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
1130+// The first argument is typically a *testing.T value.
1131+func NewMockBrokerListener(t interface {
1132+ mock.TestingT
1133+ Cleanup(func())
1134+}) *MockBrokerListener {
1135+ mock := &MockBrokerListener{}
1136+ mock.Mock.Test(t)
1137+
1138+ t.Cleanup(func() { mock.AssertExpectations(t) })
1139+
1140+ return mock
1141+}
1142+
1143+// MockBrokerListener is an autogenerated mock type for the BrokerListener type
1144+type MockBrokerListener struct {
1145+ mock.Mock
1146+}
1147+
1148+type MockBrokerListener_Expecter struct {
1149+ mock *mock.Mock
1150+}
1151+
1152+func (_m *MockBrokerListener) EXPECT() *MockBrokerListener_Expecter {
1153+ return &MockBrokerListener_Expecter{mock: &_m.Mock}
1154+}
1155+
1156+// Accept provides a mock function for the type MockBrokerListener
1157+func (_mock *MockBrokerListener) Accept() (ports.BrokerService, error) {
1158+ ret := _mock.Called()
1159+
1160+ if len(ret) == 0 {
1161+ panic("no return value specified for Accept")
1162+ }
1163+
1164+ var r0 ports.BrokerService
1165+ var r1 error
1166+ if returnFunc, ok := ret.Get(0).(func() (ports.BrokerService, error)); ok {
1167+ return returnFunc()
1168+ }
1169+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerService); ok {
1170+ r0 = returnFunc()
1171+ } else {
1172+ if ret.Get(0) != nil {
1173+ r0 = ret.Get(0).(ports.BrokerService)
1174+ }
1175+ }
1176+ if returnFunc, ok := ret.Get(1).(func() error); ok {
1177+ r1 = returnFunc()
1178+ } else {
1179+ r1 = ret.Error(1)
1180+ }
1181+ return r0, r1
1182+}
1183+
1184+// MockBrokerListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept'
1185+type MockBrokerListener_Accept_Call struct {
1186+ *mock.Call
1187+}
1188+
1189+// Accept is a helper method to define mock.On call
1190+func (_e *MockBrokerListener_Expecter) Accept() *MockBrokerListener_Accept_Call {
1191+ return &MockBrokerListener_Accept_Call{Call: _e.mock.On("Accept")}
1192+}
1193+
1194+func (_c *MockBrokerListener_Accept_Call) Run(run func()) *MockBrokerListener_Accept_Call {
1195+ _c.Call.Run(func(args mock.Arguments) {
1196+ run()
1197+ })
1198+ return _c
1199+}
1200+
1201+func (_c *MockBrokerListener_Accept_Call) Return(brokerService ports.BrokerService, err error) *MockBrokerListener_Accept_Call {
1202+ _c.Call.Return(brokerService, err)
1203+ return _c
1204+}
1205+
1206+func (_c *MockBrokerListener_Accept_Call) RunAndReturn(run func() (ports.BrokerService, error)) *MockBrokerListener_Accept_Call {
1207+ _c.Call.Return(run)
1208+ return _c
1209+}
1210+
1211+// Addr provides a mock function for the type MockBrokerListener
1212+func (_mock *MockBrokerListener) Addr() string {
1213+ ret := _mock.Called()
1214+
1215+ if len(ret) == 0 {
1216+ panic("no return value specified for Addr")
1217+ }
1218+
1219+ var r0 string
1220+ if returnFunc, ok := ret.Get(0).(func() string); ok {
1221+ r0 = returnFunc()
1222+ } else {
1223+ r0 = ret.Get(0).(string)
1224+ }
1225+ return r0
1226+}
1227+
1228+// MockBrokerListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr'
1229+type MockBrokerListener_Addr_Call struct {
1230+ *mock.Call
1231+}
1232+
1233+// Addr is a helper method to define mock.On call
1234+func (_e *MockBrokerListener_Expecter) Addr() *MockBrokerListener_Addr_Call {
1235+ return &MockBrokerListener_Addr_Call{Call: _e.mock.On("Addr")}
1236+}
1237+
1238+func (_c *MockBrokerListener_Addr_Call) Run(run func()) *MockBrokerListener_Addr_Call {
1239+ _c.Call.Run(func(args mock.Arguments) {
1240+ run()
1241+ })
1242+ return _c
1243+}
1244+
1245+func (_c *MockBrokerListener_Addr_Call) Return(s string) *MockBrokerListener_Addr_Call {
1246+ _c.Call.Return(s)
1247+ return _c
1248+}
1249+
1250+func (_c *MockBrokerListener_Addr_Call) RunAndReturn(run func() string) *MockBrokerListener_Addr_Call {
1251+ _c.Call.Return(run)
1252+ return _c
1253+}
1254+
1255+// Close provides a mock function for the type MockBrokerListener
1256+func (_mock *MockBrokerListener) Close() error {
1257+ ret := _mock.Called()
1258+
1259+ if len(ret) == 0 {
1260+ panic("no return value specified for Close")
1261+ }
1262+
1263+ var r0 error
1264+ if returnFunc, ok := ret.Get(0).(func() error); ok {
1265+ r0 = returnFunc()
1266+ } else {
1267+ r0 = ret.Error(0)
1268+ }
1269+ return r0
1270+}
1271+
1272+// MockBrokerListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
1273+type MockBrokerListener_Close_Call struct {
1274+ *mock.Call
1275+}
1276+
1277+// Close is a helper method to define mock.On call
1278+func (_e *MockBrokerListener_Expecter) Close() *MockBrokerListener_Close_Call {
1279+ return &MockBrokerListener_Close_Call{Call: _e.mock.On("Close")}
1280+}
1281+
1282+func (_c *MockBrokerListener_Close_Call) Run(run func()) *MockBrokerListener_Close_Call {
1283+ _c.Call.Run(func(args mock.Arguments) {
1284+ run()
1285+ })
1286+ return _c
1287+}
1288+
1289+func (_c *MockBrokerListener_Close_Call) Return(err error) *MockBrokerListener_Close_Call {
1290+ _c.Call.Return(err)
1291+ return _c
1292+}
1293+
1294+func (_c *MockBrokerListener_Close_Call) RunAndReturn(run func() error) *MockBrokerListener_Close_Call {
1295+ _c.Call.Return(run)
1296+ return _c
1297+}
@@ -0,0 +1,1297 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package portsmocks
6+
7+import (
8+ "context"
9+
10+ "github.com/bnema/vev/internal/ports"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMockBrokerSubscription creates a new instance of MockBrokerSubscription. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMockBrokerSubscription(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MockBrokerSubscription {
20+ mock := &MockBrokerSubscription{}
21+ mock.Mock.Test(t)
22+
23+ t.Cleanup(func() { mock.AssertExpectations(t) })
24+
25+ return mock
26+}
27+
28+// MockBrokerSubscription is an autogenerated mock type for the BrokerSubscription type
29+type MockBrokerSubscription struct {
30+ mock.Mock
31+}
32+
33+type MockBrokerSubscription_Expecter struct {
34+ mock *mock.Mock
35+}
36+
37+func (_m *MockBrokerSubscription) EXPECT() *MockBrokerSubscription_Expecter {
38+ return &MockBrokerSubscription_Expecter{mock: &_m.Mock}
39+}
40+
41+// Changed provides a mock function for the type MockBrokerSubscription
42+func (_mock *MockBrokerSubscription) Changed() <-chan struct{} {
43+ ret := _mock.Called()
44+
45+ if len(ret) == 0 {
46+ panic("no return value specified for Changed")
47+ }
48+
49+ var r0 <-chan struct{}
50+ if returnFunc, ok := ret.Get(0).(func() <-chan struct{}); ok {
51+ r0 = returnFunc()
52+ } else {
53+ if ret.Get(0) != nil {
54+ r0 = ret.Get(0).(<-chan struct{})
55+ }
56+ }
57+ return r0
58+}
59+
60+// MockBrokerSubscription_Changed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Changed'
61+type MockBrokerSubscription_Changed_Call struct {
62+ *mock.Call
63+}
64+
65+// Changed is a helper method to define mock.On call
66+func (_e *MockBrokerSubscription_Expecter) Changed() *MockBrokerSubscription_Changed_Call {
67+ return &MockBrokerSubscription_Changed_Call{Call: _e.mock.On("Changed")}
68+}
69+
70+func (_c *MockBrokerSubscription_Changed_Call) Run(run func()) *MockBrokerSubscription_Changed_Call {
71+ _c.Call.Run(func(args mock.Arguments) {
72+ run()
73+ })
74+ return _c
75+}
76+
77+func (_c *MockBrokerSubscription_Changed_Call) Return(valCh <-chan struct{}) *MockBrokerSubscription_Changed_Call {
78+ _c.Call.Return(valCh)
79+ return _c
80+}
81+
82+func (_c *MockBrokerSubscription_Changed_Call) RunAndReturn(run func() <-chan struct{}) *MockBrokerSubscription_Changed_Call {
83+ _c.Call.Return(run)
84+ return _c
85+}
86+
87+// Close provides a mock function for the type MockBrokerSubscription
88+func (_mock *MockBrokerSubscription) Close() {
89+ _mock.Called()
90+ return
91+}
92+
93+// MockBrokerSubscription_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
94+type MockBrokerSubscription_Close_Call struct {
95+ *mock.Call
96+}
97+
98+// Close is a helper method to define mock.On call
99+func (_e *MockBrokerSubscription_Expecter) Close() *MockBrokerSubscription_Close_Call {
100+ return &MockBrokerSubscription_Close_Call{Call: _e.mock.On("Close")}
101+}
102+
103+func (_c *MockBrokerSubscription_Close_Call) Run(run func()) *MockBrokerSubscription_Close_Call {
104+ _c.Call.Run(func(args mock.Arguments) {
105+ run()
106+ })
107+ return _c
108+}
109+
110+func (_c *MockBrokerSubscription_Close_Call) Return() *MockBrokerSubscription_Close_Call {
111+ _c.Call.Return()
112+ return _c
113+}
114+
115+func (_c *MockBrokerSubscription_Close_Call) RunAndReturn(run func()) *MockBrokerSubscription_Close_Call {
116+ _c.Run(run)
117+ return _c
118+}
119+
120+// NewMockBrokerService creates a new instance of MockBrokerService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
121+// The first argument is typically a *testing.T value.
122+func NewMockBrokerService(t interface {
123+ mock.TestingT
124+ Cleanup(func())
125+}) *MockBrokerService {
126+ mock := &MockBrokerService{}
127+ mock.Mock.Test(t)
128+
129+ t.Cleanup(func() { mock.AssertExpectations(t) })
130+
131+ return mock
132+}
133+
134+// MockBrokerService is an autogenerated mock type for the BrokerService type
135+type MockBrokerService struct {
136+ mock.Mock
137+}
138+
139+type MockBrokerService_Expecter struct {
140+ mock *mock.Mock
141+}
142+
143+func (_m *MockBrokerService) EXPECT() *MockBrokerService_Expecter {
144+ return &MockBrokerService_Expecter{mock: &_m.Mock}
145+}
146+
147+// AddHost provides a mock function for the type MockBrokerService
148+func (_mock *MockBrokerService) AddHost(ctx context.Context, target string) error {
149+ ret := _mock.Called(ctx, target)
150+
151+ if len(ret) == 0 {
152+ panic("no return value specified for AddHost")
153+ }
154+
155+ var r0 error
156+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
157+ r0 = returnFunc(ctx, target)
158+ } else {
159+ r0 = ret.Error(0)
160+ }
161+ return r0
162+}
163+
164+// MockBrokerService_AddHost_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddHost'
165+type MockBrokerService_AddHost_Call struct {
166+ *mock.Call
167+}
168+
169+// AddHost is a helper method to define mock.On call
170+// - ctx context.Context
171+// - target string
172+func (_e *MockBrokerService_Expecter) AddHost(ctx any, target any) *MockBrokerService_AddHost_Call {
173+ return &MockBrokerService_AddHost_Call{Call: _e.mock.On("AddHost", ctx, target)}
174+}
175+
176+func (_c *MockBrokerService_AddHost_Call) Run(run func(ctx context.Context, target string)) *MockBrokerService_AddHost_Call {
177+ _c.Call.Run(func(args mock.Arguments) {
178+ var arg0 context.Context
179+ if args[0] != nil {
180+ arg0 = args[0].(context.Context)
181+ }
182+ var arg1 string
183+ if args[1] != nil {
184+ arg1 = args[1].(string)
185+ }
186+ run(
187+ arg0,
188+ arg1,
189+ )
190+ })
191+ return _c
192+}
193+
194+func (_c *MockBrokerService_AddHost_Call) Return(err error) *MockBrokerService_AddHost_Call {
195+ _c.Call.Return(err)
196+ return _c
197+}
198+
199+func (_c *MockBrokerService_AddHost_Call) RunAndReturn(run func(ctx context.Context, target string) error) *MockBrokerService_AddHost_Call {
200+ _c.Call.Return(run)
201+ return _c
202+}
203+
204+// Close provides a mock function for the type MockBrokerService
205+func (_mock *MockBrokerService) Close() error {
206+ ret := _mock.Called()
207+
208+ if len(ret) == 0 {
209+ panic("no return value specified for Close")
210+ }
211+
212+ var r0 error
213+ if returnFunc, ok := ret.Get(0).(func() error); ok {
214+ r0 = returnFunc()
215+ } else {
216+ r0 = ret.Error(0)
217+ }
218+ return r0
219+}
220+
221+// MockBrokerService_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
222+type MockBrokerService_Close_Call struct {
223+ *mock.Call
224+}
225+
226+// Close is a helper method to define mock.On call
227+func (_e *MockBrokerService_Expecter) Close() *MockBrokerService_Close_Call {
228+ return &MockBrokerService_Close_Call{Call: _e.mock.On("Close")}
229+}
230+
231+func (_c *MockBrokerService_Close_Call) Run(run func()) *MockBrokerService_Close_Call {
232+ _c.Call.Run(func(args mock.Arguments) {
233+ run()
234+ })
235+ return _c
236+}
237+
238+func (_c *MockBrokerService_Close_Call) Return(err error) *MockBrokerService_Close_Call {
239+ _c.Call.Return(err)
240+ return _c
241+}
242+
243+func (_c *MockBrokerService_Close_Call) RunAndReturn(run func() error) *MockBrokerService_Close_Call {
244+ _c.Call.Return(run)
245+ return _c
246+}
247+
248+// CloseStream provides a mock function for the type MockBrokerService
249+func (_mock *MockBrokerService) CloseStream(connection ports.BrokerConnectionID, stream ports.BrokerStreamID) error {
250+ ret := _mock.Called(connection, stream)
251+
252+ if len(ret) == 0 {
253+ panic("no return value specified for CloseStream")
254+ }
255+
256+ var r0 error
257+ if returnFunc, ok := ret.Get(0).(func(ports.BrokerConnectionID, ports.BrokerStreamID) error); ok {
258+ r0 = returnFunc(connection, stream)
259+ } else {
260+ r0 = ret.Error(0)
261+ }
262+ return r0
263+}
264+
265+// MockBrokerService_CloseStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseStream'
266+type MockBrokerService_CloseStream_Call struct {
267+ *mock.Call
268+}
269+
270+// CloseStream is a helper method to define mock.On call
271+// - connection ports.BrokerConnectionID
272+// - stream ports.BrokerStreamID
273+func (_e *MockBrokerService_Expecter) CloseStream(connection any, stream any) *MockBrokerService_CloseStream_Call {
274+ return &MockBrokerService_CloseStream_Call{Call: _e.mock.On("CloseStream", connection, stream)}
275+}
276+
277+func (_c *MockBrokerService_CloseStream_Call) Run(run func(connection ports.BrokerConnectionID, stream ports.BrokerStreamID)) *MockBrokerService_CloseStream_Call {
278+ _c.Call.Run(func(args mock.Arguments) {
279+ var arg0 ports.BrokerConnectionID
280+ if args[0] != nil {
281+ arg0 = args[0].(ports.BrokerConnectionID)
282+ }
283+ var arg1 ports.BrokerStreamID
284+ if args[1] != nil {
285+ arg1 = args[1].(ports.BrokerStreamID)
286+ }
287+ run(
288+ arg0,
289+ arg1,
290+ )
291+ })
292+ return _c
293+}
294+
295+func (_c *MockBrokerService_CloseStream_Call) Return(err error) *MockBrokerService_CloseStream_Call {
296+ _c.Call.Return(err)
297+ return _c
298+}
299+
300+func (_c *MockBrokerService_CloseStream_Call) RunAndReturn(run func(connection ports.BrokerConnectionID, stream ports.BrokerStreamID) error) *MockBrokerService_CloseStream_Call {
301+ _c.Call.Return(run)
302+ return _c
303+}
304+
305+// ConnectionID provides a mock function for the type MockBrokerService
306+func (_mock *MockBrokerService) ConnectionID() ports.BrokerConnectionID {
307+ ret := _mock.Called()
308+
309+ if len(ret) == 0 {
310+ panic("no return value specified for ConnectionID")
311+ }
312+
313+ var r0 ports.BrokerConnectionID
314+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerConnectionID); ok {
315+ r0 = returnFunc()
316+ } else {
317+ if ret.Get(0) != nil {
318+ r0 = ret.Get(0).(ports.BrokerConnectionID)
319+ }
320+ }
321+ return r0
322+}
323+
324+// MockBrokerService_ConnectionID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConnectionID'
325+type MockBrokerService_ConnectionID_Call struct {
326+ *mock.Call
327+}
328+
329+// ConnectionID is a helper method to define mock.On call
330+func (_e *MockBrokerService_Expecter) ConnectionID() *MockBrokerService_ConnectionID_Call {
331+ return &MockBrokerService_ConnectionID_Call{Call: _e.mock.On("ConnectionID")}
332+}
333+
334+func (_c *MockBrokerService_ConnectionID_Call) Run(run func()) *MockBrokerService_ConnectionID_Call {
335+ _c.Call.Run(func(args mock.Arguments) {
336+ run()
337+ })
338+ return _c
339+}
340+
341+func (_c *MockBrokerService_ConnectionID_Call) Return(brokerConnectionID ports.BrokerConnectionID) *MockBrokerService_ConnectionID_Call {
342+ _c.Call.Return(brokerConnectionID)
343+ return _c
344+}
345+
346+func (_c *MockBrokerService_ConnectionID_Call) RunAndReturn(run func() ports.BrokerConnectionID) *MockBrokerService_ConnectionID_Call {
347+ _c.Call.Return(run)
348+ return _c
349+}
350+
351+// OpenStream provides a mock function for the type MockBrokerService
352+func (_mock *MockBrokerService) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error) {
353+ ret := _mock.Called(ctx, request)
354+
355+ if len(ret) == 0 {
356+ panic("no return value specified for OpenStream")
357+ }
358+
359+ var r0 ports.ClientConnection
360+ var r1 error
361+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)); ok {
362+ return returnFunc(ctx, request)
363+ }
364+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.ClientConnection); ok {
365+ r0 = returnFunc(ctx, request)
366+ } else {
367+ if ret.Get(0) != nil {
368+ r0 = ret.Get(0).(ports.ClientConnection)
369+ }
370+ }
371+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
372+ r1 = returnFunc(ctx, request)
373+ } else {
374+ r1 = ret.Error(1)
375+ }
376+ return r0, r1
377+}
378+
379+// MockBrokerService_OpenStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenStream'
380+type MockBrokerService_OpenStream_Call struct {
381+ *mock.Call
382+}
383+
384+// OpenStream is a helper method to define mock.On call
385+// - ctx context.Context
386+// - request ports.BrokerOpenStreamRequest
387+func (_e *MockBrokerService_Expecter) OpenStream(ctx any, request any) *MockBrokerService_OpenStream_Call {
388+ return &MockBrokerService_OpenStream_Call{Call: _e.mock.On("OpenStream", ctx, request)}
389+}
390+
391+func (_c *MockBrokerService_OpenStream_Call) Run(run func(ctx context.Context, request ports.BrokerOpenStreamRequest)) *MockBrokerService_OpenStream_Call {
392+ _c.Call.Run(func(args mock.Arguments) {
393+ var arg0 context.Context
394+ if args[0] != nil {
395+ arg0 = args[0].(context.Context)
396+ }
397+ var arg1 ports.BrokerOpenStreamRequest
398+ if args[1] != nil {
399+ arg1 = args[1].(ports.BrokerOpenStreamRequest)
400+ }
401+ run(
402+ arg0,
403+ arg1,
404+ )
405+ })
406+ return _c
407+}
408+
409+func (_c *MockBrokerService_OpenStream_Call) Return(clientConnection ports.ClientConnection, err error) *MockBrokerService_OpenStream_Call {
410+ _c.Call.Return(clientConnection, err)
411+ return _c
412+}
413+
414+func (_c *MockBrokerService_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)) *MockBrokerService_OpenStream_Call {
415+ _c.Call.Return(run)
416+ return _c
417+}
418+
419+// RemoveHost provides a mock function for the type MockBrokerService
420+func (_mock *MockBrokerService) RemoveHost(ctx context.Context, target string) (bool, error) {
421+ ret := _mock.Called(ctx, target)
422+
423+ if len(ret) == 0 {
424+ panic("no return value specified for RemoveHost")
425+ }
426+
427+ var r0 bool
428+ var r1 error
429+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
430+ return returnFunc(ctx, target)
431+ }
432+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok {
433+ r0 = returnFunc(ctx, target)
434+ } else {
435+ r0 = ret.Get(0).(bool)
436+ }
437+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
438+ r1 = returnFunc(ctx, target)
439+ } else {
440+ r1 = ret.Error(1)
441+ }
442+ return r0, r1
443+}
444+
445+// MockBrokerService_RemoveHost_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveHost'
446+type MockBrokerService_RemoveHost_Call struct {
447+ *mock.Call
448+}
449+
450+// RemoveHost is a helper method to define mock.On call
451+// - ctx context.Context
452+// - target string
453+func (_e *MockBrokerService_Expecter) RemoveHost(ctx any, target any) *MockBrokerService_RemoveHost_Call {
454+ return &MockBrokerService_RemoveHost_Call{Call: _e.mock.On("RemoveHost", ctx, target)}
455+}
456+
457+func (_c *MockBrokerService_RemoveHost_Call) Run(run func(ctx context.Context, target string)) *MockBrokerService_RemoveHost_Call {
458+ _c.Call.Run(func(args mock.Arguments) {
459+ var arg0 context.Context
460+ if args[0] != nil {
461+ arg0 = args[0].(context.Context)
462+ }
463+ var arg1 string
464+ if args[1] != nil {
465+ arg1 = args[1].(string)
466+ }
467+ run(
468+ arg0,
469+ arg1,
470+ )
471+ })
472+ return _c
473+}
474+
475+func (_c *MockBrokerService_RemoveHost_Call) Return(b bool, err error) *MockBrokerService_RemoveHost_Call {
476+ _c.Call.Return(b, err)
477+ return _c
478+}
479+
480+func (_c *MockBrokerService_RemoveHost_Call) RunAndReturn(run func(ctx context.Context, target string) (bool, error)) *MockBrokerService_RemoveHost_Call {
481+ _c.Call.Return(run)
482+ return _c
483+}
484+
485+// RequestReconcile provides a mock function for the type MockBrokerService
486+func (_mock *MockBrokerService) RequestReconcile(endpoint string) {
487+ _mock.Called(endpoint)
488+ return
489+}
490+
491+// MockBrokerService_RequestReconcile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequestReconcile'
492+type MockBrokerService_RequestReconcile_Call struct {
493+ *mock.Call
494+}
495+
496+// RequestReconcile is a helper method to define mock.On call
497+// - endpoint string
498+func (_e *MockBrokerService_Expecter) RequestReconcile(endpoint any) *MockBrokerService_RequestReconcile_Call {
499+ return &MockBrokerService_RequestReconcile_Call{Call: _e.mock.On("RequestReconcile", endpoint)}
500+}
501+
502+func (_c *MockBrokerService_RequestReconcile_Call) Run(run func(endpoint string)) *MockBrokerService_RequestReconcile_Call {
503+ _c.Call.Run(func(args mock.Arguments) {
504+ var arg0 string
505+ if args[0] != nil {
506+ arg0 = args[0].(string)
507+ }
508+ run(
509+ arg0,
510+ )
511+ })
512+ return _c
513+}
514+
515+func (_c *MockBrokerService_RequestReconcile_Call) Return() *MockBrokerService_RequestReconcile_Call {
516+ _c.Call.Return()
517+ return _c
518+}
519+
520+func (_c *MockBrokerService_RequestReconcile_Call) RunAndReturn(run func(endpoint string)) *MockBrokerService_RequestReconcile_Call {
521+ _c.Run(run)
522+ return _c
523+}
524+
525+// Snapshot provides a mock function for the type MockBrokerService
526+func (_mock *MockBrokerService) Snapshot() ports.BrokerSnapshot {
527+ ret := _mock.Called()
528+
529+ if len(ret) == 0 {
530+ panic("no return value specified for Snapshot")
531+ }
532+
533+ var r0 ports.BrokerSnapshot
534+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerSnapshot); ok {
535+ r0 = returnFunc()
536+ } else {
537+ r0 = ret.Get(0).(ports.BrokerSnapshot)
538+ }
539+ return r0
540+}
541+
542+// MockBrokerService_Snapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Snapshot'
543+type MockBrokerService_Snapshot_Call struct {
544+ *mock.Call
545+}
546+
547+// Snapshot is a helper method to define mock.On call
548+func (_e *MockBrokerService_Expecter) Snapshot() *MockBrokerService_Snapshot_Call {
549+ return &MockBrokerService_Snapshot_Call{Call: _e.mock.On("Snapshot")}
550+}
551+
552+func (_c *MockBrokerService_Snapshot_Call) Run(run func()) *MockBrokerService_Snapshot_Call {
553+ _c.Call.Run(func(args mock.Arguments) {
554+ run()
555+ })
556+ return _c
557+}
558+
559+func (_c *MockBrokerService_Snapshot_Call) Return(brokerSnapshot ports.BrokerSnapshot) *MockBrokerService_Snapshot_Call {
560+ _c.Call.Return(brokerSnapshot)
561+ return _c
562+}
563+
564+func (_c *MockBrokerService_Snapshot_Call) RunAndReturn(run func() ports.BrokerSnapshot) *MockBrokerService_Snapshot_Call {
565+ _c.Call.Return(run)
566+ return _c
567+}
568+
569+// Subscribe provides a mock function for the type MockBrokerService
570+func (_mock *MockBrokerService) Subscribe() (ports.BrokerSubscription, error) {
571+ ret := _mock.Called()
572+
573+ if len(ret) == 0 {
574+ panic("no return value specified for Subscribe")
575+ }
576+
577+ var r0 ports.BrokerSubscription
578+ var r1 error
579+ if returnFunc, ok := ret.Get(0).(func() (ports.BrokerSubscription, error)); ok {
580+ return returnFunc()
581+ }
582+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerSubscription); ok {
583+ r0 = returnFunc()
584+ } else {
585+ if ret.Get(0) != nil {
586+ r0 = ret.Get(0).(ports.BrokerSubscription)
587+ }
588+ }
589+ if returnFunc, ok := ret.Get(1).(func() error); ok {
590+ r1 = returnFunc()
591+ } else {
592+ r1 = ret.Error(1)
593+ }
594+ return r0, r1
595+}
596+
597+// MockBrokerService_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
598+type MockBrokerService_Subscribe_Call struct {
599+ *mock.Call
600+}
601+
602+// Subscribe is a helper method to define mock.On call
603+func (_e *MockBrokerService_Expecter) Subscribe() *MockBrokerService_Subscribe_Call {
604+ return &MockBrokerService_Subscribe_Call{Call: _e.mock.On("Subscribe")}
605+}
606+
607+func (_c *MockBrokerService_Subscribe_Call) Run(run func()) *MockBrokerService_Subscribe_Call {
608+ _c.Call.Run(func(args mock.Arguments) {
609+ run()
610+ })
611+ return _c
612+}
613+
614+func (_c *MockBrokerService_Subscribe_Call) Return(brokerSubscription ports.BrokerSubscription, err error) *MockBrokerService_Subscribe_Call {
615+ _c.Call.Return(brokerSubscription, err)
616+ return _c
617+}
618+
619+func (_c *MockBrokerService_Subscribe_Call) RunAndReturn(run func() (ports.BrokerSubscription, error)) *MockBrokerService_Subscribe_Call {
620+ _c.Call.Return(run)
621+ return _c
622+}
623+
624+// NewMockBrokerSnapshotStore creates a new instance of MockBrokerSnapshotStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
625+// The first argument is typically a *testing.T value.
626+func NewMockBrokerSnapshotStore(t interface {
627+ mock.TestingT
628+ Cleanup(func())
629+}) *MockBrokerSnapshotStore {
630+ mock := &MockBrokerSnapshotStore{}
631+ mock.Mock.Test(t)
632+
633+ t.Cleanup(func() { mock.AssertExpectations(t) })
634+
635+ return mock
636+}
637+
638+// MockBrokerSnapshotStore is an autogenerated mock type for the BrokerSnapshotStore type
639+type MockBrokerSnapshotStore struct {
640+ mock.Mock
641+}
642+
643+type MockBrokerSnapshotStore_Expecter struct {
644+ mock *mock.Mock
645+}
646+
647+func (_m *MockBrokerSnapshotStore) EXPECT() *MockBrokerSnapshotStore_Expecter {
648+ return &MockBrokerSnapshotStore_Expecter{mock: &_m.Mock}
649+}
650+
651+// Load provides a mock function for the type MockBrokerSnapshotStore
652+func (_mock *MockBrokerSnapshotStore) Load() (ports.BrokerSnapshot, error) {
653+ ret := _mock.Called()
654+
655+ if len(ret) == 0 {
656+ panic("no return value specified for Load")
657+ }
658+
659+ var r0 ports.BrokerSnapshot
660+ var r1 error
661+ if returnFunc, ok := ret.Get(0).(func() (ports.BrokerSnapshot, error)); ok {
662+ return returnFunc()
663+ }
664+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerSnapshot); ok {
665+ r0 = returnFunc()
666+ } else {
667+ r0 = ret.Get(0).(ports.BrokerSnapshot)
668+ }
669+ if returnFunc, ok := ret.Get(1).(func() error); ok {
670+ r1 = returnFunc()
671+ } else {
672+ r1 = ret.Error(1)
673+ }
674+ return r0, r1
675+}
676+
677+// MockBrokerSnapshotStore_Load_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Load'
678+type MockBrokerSnapshotStore_Load_Call struct {
679+ *mock.Call
680+}
681+
682+// Load is a helper method to define mock.On call
683+func (_e *MockBrokerSnapshotStore_Expecter) Load() *MockBrokerSnapshotStore_Load_Call {
684+ return &MockBrokerSnapshotStore_Load_Call{Call: _e.mock.On("Load")}
685+}
686+
687+func (_c *MockBrokerSnapshotStore_Load_Call) Run(run func()) *MockBrokerSnapshotStore_Load_Call {
688+ _c.Call.Run(func(args mock.Arguments) {
689+ run()
690+ })
691+ return _c
692+}
693+
694+func (_c *MockBrokerSnapshotStore_Load_Call) Return(brokerSnapshot ports.BrokerSnapshot, err error) *MockBrokerSnapshotStore_Load_Call {
695+ _c.Call.Return(brokerSnapshot, err)
696+ return _c
697+}
698+
699+func (_c *MockBrokerSnapshotStore_Load_Call) RunAndReturn(run func() (ports.BrokerSnapshot, error)) *MockBrokerSnapshotStore_Load_Call {
700+ _c.Call.Return(run)
701+ return _c
702+}
703+
704+// Store provides a mock function for the type MockBrokerSnapshotStore
705+func (_mock *MockBrokerSnapshotStore) Store(brokerSnapshot ports.BrokerSnapshot) error {
706+ ret := _mock.Called(brokerSnapshot)
707+
708+ if len(ret) == 0 {
709+ panic("no return value specified for Store")
710+ }
711+
712+ var r0 error
713+ if returnFunc, ok := ret.Get(0).(func(ports.BrokerSnapshot) error); ok {
714+ r0 = returnFunc(brokerSnapshot)
715+ } else {
716+ r0 = ret.Error(0)
717+ }
718+ return r0
719+}
720+
721+// MockBrokerSnapshotStore_Store_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Store'
722+type MockBrokerSnapshotStore_Store_Call struct {
723+ *mock.Call
724+}
725+
726+// Store is a helper method to define mock.On call
727+// - brokerSnapshot ports.BrokerSnapshot
728+func (_e *MockBrokerSnapshotStore_Expecter) Store(brokerSnapshot any) *MockBrokerSnapshotStore_Store_Call {
729+ return &MockBrokerSnapshotStore_Store_Call{Call: _e.mock.On("Store", brokerSnapshot)}
730+}
731+
732+func (_c *MockBrokerSnapshotStore_Store_Call) Run(run func(brokerSnapshot ports.BrokerSnapshot)) *MockBrokerSnapshotStore_Store_Call {
733+ _c.Call.Run(func(args mock.Arguments) {
734+ var arg0 ports.BrokerSnapshot
735+ if args[0] != nil {
736+ arg0 = args[0].(ports.BrokerSnapshot)
737+ }
738+ run(
739+ arg0,
740+ )
741+ })
742+ return _c
743+}
744+
745+func (_c *MockBrokerSnapshotStore_Store_Call) Return(err error) *MockBrokerSnapshotStore_Store_Call {
746+ _c.Call.Return(err)
747+ return _c
748+}
749+
750+func (_c *MockBrokerSnapshotStore_Store_Call) RunAndReturn(run func(brokerSnapshot ports.BrokerSnapshot) error) *MockBrokerSnapshotStore_Store_Call {
751+ _c.Call.Return(run)
752+ return _c
753+}
754+
755+// NewMockBrokerPhysicalConnection creates a new instance of MockBrokerPhysicalConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
756+// The first argument is typically a *testing.T value.
757+func NewMockBrokerPhysicalConnection(t interface {
758+ mock.TestingT
759+ Cleanup(func())
760+}) *MockBrokerPhysicalConnection {
761+ mock := &MockBrokerPhysicalConnection{}
762+ mock.Mock.Test(t)
763+
764+ t.Cleanup(func() { mock.AssertExpectations(t) })
765+
766+ return mock
767+}
768+
769+// MockBrokerPhysicalConnection is an autogenerated mock type for the BrokerPhysicalConnection type
770+type MockBrokerPhysicalConnection struct {
771+ mock.Mock
772+}
773+
774+type MockBrokerPhysicalConnection_Expecter struct {
775+ mock *mock.Mock
776+}
777+
778+func (_m *MockBrokerPhysicalConnection) EXPECT() *MockBrokerPhysicalConnection_Expecter {
779+ return &MockBrokerPhysicalConnection_Expecter{mock: &_m.Mock}
780+}
781+
782+// Close provides a mock function for the type MockBrokerPhysicalConnection
783+func (_mock *MockBrokerPhysicalConnection) Close() error {
784+ ret := _mock.Called()
785+
786+ if len(ret) == 0 {
787+ panic("no return value specified for Close")
788+ }
789+
790+ var r0 error
791+ if returnFunc, ok := ret.Get(0).(func() error); ok {
792+ r0 = returnFunc()
793+ } else {
794+ r0 = ret.Error(0)
795+ }
796+ return r0
797+}
798+
799+// MockBrokerPhysicalConnection_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
800+type MockBrokerPhysicalConnection_Close_Call struct {
801+ *mock.Call
802+}
803+
804+// Close is a helper method to define mock.On call
805+func (_e *MockBrokerPhysicalConnection_Expecter) Close() *MockBrokerPhysicalConnection_Close_Call {
806+ return &MockBrokerPhysicalConnection_Close_Call{Call: _e.mock.On("Close")}
807+}
808+
809+func (_c *MockBrokerPhysicalConnection_Close_Call) Run(run func()) *MockBrokerPhysicalConnection_Close_Call {
810+ _c.Call.Run(func(args mock.Arguments) {
811+ run()
812+ })
813+ return _c
814+}
815+
816+func (_c *MockBrokerPhysicalConnection_Close_Call) Return(err error) *MockBrokerPhysicalConnection_Close_Call {
817+ _c.Call.Return(err)
818+ return _c
819+}
820+
821+func (_c *MockBrokerPhysicalConnection_Close_Call) RunAndReturn(run func() error) *MockBrokerPhysicalConnection_Close_Call {
822+ _c.Call.Return(run)
823+ return _c
824+}
825+
826+// Identity provides a mock function for the type MockBrokerPhysicalConnection
827+func (_mock *MockBrokerPhysicalConnection) Identity() ports.BrokerDaemonIdentity {
828+ ret := _mock.Called()
829+
830+ if len(ret) == 0 {
831+ panic("no return value specified for Identity")
832+ }
833+
834+ var r0 ports.BrokerDaemonIdentity
835+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerDaemonIdentity); ok {
836+ r0 = returnFunc()
837+ } else {
838+ r0 = ret.Get(0).(ports.BrokerDaemonIdentity)
839+ }
840+ return r0
841+}
842+
843+// MockBrokerPhysicalConnection_Identity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Identity'
844+type MockBrokerPhysicalConnection_Identity_Call struct {
845+ *mock.Call
846+}
847+
848+// Identity is a helper method to define mock.On call
849+func (_e *MockBrokerPhysicalConnection_Expecter) Identity() *MockBrokerPhysicalConnection_Identity_Call {
850+ return &MockBrokerPhysicalConnection_Identity_Call{Call: _e.mock.On("Identity")}
851+}
852+
853+func (_c *MockBrokerPhysicalConnection_Identity_Call) Run(run func()) *MockBrokerPhysicalConnection_Identity_Call {
854+ _c.Call.Run(func(args mock.Arguments) {
855+ run()
856+ })
857+ return _c
858+}
859+
860+func (_c *MockBrokerPhysicalConnection_Identity_Call) Return(brokerDaemonIdentity ports.BrokerDaemonIdentity) *MockBrokerPhysicalConnection_Identity_Call {
861+ _c.Call.Return(brokerDaemonIdentity)
862+ return _c
863+}
864+
865+func (_c *MockBrokerPhysicalConnection_Identity_Call) RunAndReturn(run func() ports.BrokerDaemonIdentity) *MockBrokerPhysicalConnection_Identity_Call {
866+ _c.Call.Return(run)
867+ return _c
868+}
869+
870+// Incarnation provides a mock function for the type MockBrokerPhysicalConnection
871+func (_mock *MockBrokerPhysicalConnection) Incarnation() ports.BrokerDaemonIncarnation {
872+ ret := _mock.Called()
873+
874+ if len(ret) == 0 {
875+ panic("no return value specified for Incarnation")
876+ }
877+
878+ var r0 ports.BrokerDaemonIncarnation
879+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerDaemonIncarnation); ok {
880+ r0 = returnFunc()
881+ } else {
882+ if ret.Get(0) != nil {
883+ r0 = ret.Get(0).(ports.BrokerDaemonIncarnation)
884+ }
885+ }
886+ return r0
887+}
888+
889+// MockBrokerPhysicalConnection_Incarnation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Incarnation'
890+type MockBrokerPhysicalConnection_Incarnation_Call struct {
891+ *mock.Call
892+}
893+
894+// Incarnation is a helper method to define mock.On call
895+func (_e *MockBrokerPhysicalConnection_Expecter) Incarnation() *MockBrokerPhysicalConnection_Incarnation_Call {
896+ return &MockBrokerPhysicalConnection_Incarnation_Call{Call: _e.mock.On("Incarnation")}
897+}
898+
899+func (_c *MockBrokerPhysicalConnection_Incarnation_Call) Run(run func()) *MockBrokerPhysicalConnection_Incarnation_Call {
900+ _c.Call.Run(func(args mock.Arguments) {
901+ run()
902+ })
903+ return _c
904+}
905+
906+func (_c *MockBrokerPhysicalConnection_Incarnation_Call) Return(brokerDaemonIncarnation ports.BrokerDaemonIncarnation) *MockBrokerPhysicalConnection_Incarnation_Call {
907+ _c.Call.Return(brokerDaemonIncarnation)
908+ return _c
909+}
910+
911+func (_c *MockBrokerPhysicalConnection_Incarnation_Call) RunAndReturn(run func() ports.BrokerDaemonIncarnation) *MockBrokerPhysicalConnection_Incarnation_Call {
912+ _c.Call.Return(run)
913+ return _c
914+}
915+
916+// OpenStream provides a mock function for the type MockBrokerPhysicalConnection
917+func (_mock *MockBrokerPhysicalConnection) OpenStream(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error) {
918+ ret := _mock.Called(ctx, request)
919+
920+ if len(ret) == 0 {
921+ panic("no return value specified for OpenStream")
922+ }
923+
924+ var r0 ports.ClientConnection
925+ var r1 error
926+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)); ok {
927+ return returnFunc(ctx, request)
928+ }
929+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.BrokerOpenStreamRequest) ports.ClientConnection); ok {
930+ r0 = returnFunc(ctx, request)
931+ } else {
932+ if ret.Get(0) != nil {
933+ r0 = ret.Get(0).(ports.ClientConnection)
934+ }
935+ }
936+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.BrokerOpenStreamRequest) error); ok {
937+ r1 = returnFunc(ctx, request)
938+ } else {
939+ r1 = ret.Error(1)
940+ }
941+ return r0, r1
942+}
943+
944+// MockBrokerPhysicalConnection_OpenStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenStream'
945+type MockBrokerPhysicalConnection_OpenStream_Call struct {
946+ *mock.Call
947+}
948+
949+// OpenStream is a helper method to define mock.On call
950+// - ctx context.Context
951+// - request ports.BrokerOpenStreamRequest
952+func (_e *MockBrokerPhysicalConnection_Expecter) OpenStream(ctx any, request any) *MockBrokerPhysicalConnection_OpenStream_Call {
953+ return &MockBrokerPhysicalConnection_OpenStream_Call{Call: _e.mock.On("OpenStream", ctx, request)}
954+}
955+
956+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Run(run func(ctx context.Context, request ports.BrokerOpenStreamRequest)) *MockBrokerPhysicalConnection_OpenStream_Call {
957+ _c.Call.Run(func(args mock.Arguments) {
958+ var arg0 context.Context
959+ if args[0] != nil {
960+ arg0 = args[0].(context.Context)
961+ }
962+ var arg1 ports.BrokerOpenStreamRequest
963+ if args[1] != nil {
964+ arg1 = args[1].(ports.BrokerOpenStreamRequest)
965+ }
966+ run(
967+ arg0,
968+ arg1,
969+ )
970+ })
971+ return _c
972+}
973+
974+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) Return(clientConnection ports.ClientConnection, err error) *MockBrokerPhysicalConnection_OpenStream_Call {
975+ _c.Call.Return(clientConnection, err)
976+ return _c
977+}
978+
979+func (_c *MockBrokerPhysicalConnection_OpenStream_Call) RunAndReturn(run func(ctx context.Context, request ports.BrokerOpenStreamRequest) (ports.ClientConnection, error)) *MockBrokerPhysicalConnection_OpenStream_Call {
980+ _c.Call.Return(run)
981+ return _c
982+}
983+
984+// Policy provides a mock function for the type MockBrokerPhysicalConnection
985+func (_mock *MockBrokerPhysicalConnection) Policy() ports.BrokerPolicy {
986+ ret := _mock.Called()
987+
988+ if len(ret) == 0 {
989+ panic("no return value specified for Policy")
990+ }
991+
992+ var r0 ports.BrokerPolicy
993+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerPolicy); ok {
994+ r0 = returnFunc()
995+ } else {
996+ r0 = ret.Get(0).(ports.BrokerPolicy)
997+ }
998+ return r0
999+}
1000+
1001+// MockBrokerPhysicalConnection_Policy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Policy'
1002+type MockBrokerPhysicalConnection_Policy_Call struct {
1003+ *mock.Call
1004+}
1005+
1006+// Policy is a helper method to define mock.On call
1007+func (_e *MockBrokerPhysicalConnection_Expecter) Policy() *MockBrokerPhysicalConnection_Policy_Call {
1008+ return &MockBrokerPhysicalConnection_Policy_Call{Call: _e.mock.On("Policy")}
1009+}
1010+
1011+func (_c *MockBrokerPhysicalConnection_Policy_Call) Run(run func()) *MockBrokerPhysicalConnection_Policy_Call {
1012+ _c.Call.Run(func(args mock.Arguments) {
1013+ run()
1014+ })
1015+ return _c
1016+}
1017+
1018+func (_c *MockBrokerPhysicalConnection_Policy_Call) Return(brokerPolicy ports.BrokerPolicy) *MockBrokerPhysicalConnection_Policy_Call {
1019+ _c.Call.Return(brokerPolicy)
1020+ return _c
1021+}
1022+
1023+func (_c *MockBrokerPhysicalConnection_Policy_Call) RunAndReturn(run func() ports.BrokerPolicy) *MockBrokerPhysicalConnection_Policy_Call {
1024+ _c.Call.Return(run)
1025+ return _c
1026+}
1027+
1028+// NewMockBrokerEndpointConnector creates a new instance of MockBrokerEndpointConnector. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
1029+// The first argument is typically a *testing.T value.
1030+func NewMockBrokerEndpointConnector(t interface {
1031+ mock.TestingT
1032+ Cleanup(func())
1033+}) *MockBrokerEndpointConnector {
1034+ mock := &MockBrokerEndpointConnector{}
1035+ mock.Mock.Test(t)
1036+
1037+ t.Cleanup(func() { mock.AssertExpectations(t) })
1038+
1039+ return mock
1040+}
1041+
1042+// MockBrokerEndpointConnector is an autogenerated mock type for the BrokerEndpointConnector type
1043+type MockBrokerEndpointConnector struct {
1044+ mock.Mock
1045+}
1046+
1047+type MockBrokerEndpointConnector_Expecter struct {
1048+ mock *mock.Mock
1049+}
1050+
1051+func (_m *MockBrokerEndpointConnector) EXPECT() *MockBrokerEndpointConnector_Expecter {
1052+ return &MockBrokerEndpointConnector_Expecter{mock: &_m.Mock}
1053+}
1054+
1055+// Connect provides a mock function for the type MockBrokerEndpointConnector
1056+func (_mock *MockBrokerEndpointConnector) Connect(ctx context.Context, endpoint string, policy ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error) {
1057+ ret := _mock.Called(ctx, endpoint, policy)
1058+
1059+ if len(ret) == 0 {
1060+ panic("no return value specified for Connect")
1061+ }
1062+
1063+ var r0 ports.BrokerPhysicalConnection
1064+ var r1 error
1065+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error)); ok {
1066+ return returnFunc(ctx, endpoint, policy)
1067+ }
1068+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.BrokerPolicy) ports.BrokerPhysicalConnection); ok {
1069+ r0 = returnFunc(ctx, endpoint, policy)
1070+ } else {
1071+ if ret.Get(0) != nil {
1072+ r0 = ret.Get(0).(ports.BrokerPhysicalConnection)
1073+ }
1074+ }
1075+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, ports.BrokerPolicy) error); ok {
1076+ r1 = returnFunc(ctx, endpoint, policy)
1077+ } else {
1078+ r1 = ret.Error(1)
1079+ }
1080+ return r0, r1
1081+}
1082+
1083+// MockBrokerEndpointConnector_Connect_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Connect'
1084+type MockBrokerEndpointConnector_Connect_Call struct {
1085+ *mock.Call
1086+}
1087+
1088+// Connect is a helper method to define mock.On call
1089+// - ctx context.Context
1090+// - endpoint string
1091+// - policy ports.BrokerPolicy
1092+func (_e *MockBrokerEndpointConnector_Expecter) Connect(ctx any, endpoint any, policy any) *MockBrokerEndpointConnector_Connect_Call {
1093+ return &MockBrokerEndpointConnector_Connect_Call{Call: _e.mock.On("Connect", ctx, endpoint, policy)}
1094+}
1095+
1096+func (_c *MockBrokerEndpointConnector_Connect_Call) Run(run func(ctx context.Context, endpoint string, policy ports.BrokerPolicy)) *MockBrokerEndpointConnector_Connect_Call {
1097+ _c.Call.Run(func(args mock.Arguments) {
1098+ var arg0 context.Context
1099+ if args[0] != nil {
1100+ arg0 = args[0].(context.Context)
1101+ }
1102+ var arg1 string
1103+ if args[1] != nil {
1104+ arg1 = args[1].(string)
1105+ }
1106+ var arg2 ports.BrokerPolicy
1107+ if args[2] != nil {
1108+ arg2 = args[2].(ports.BrokerPolicy)
1109+ }
1110+ run(
1111+ arg0,
1112+ arg1,
1113+ arg2,
1114+ )
1115+ })
1116+ return _c
1117+}
1118+
1119+func (_c *MockBrokerEndpointConnector_Connect_Call) Return(brokerPhysicalConnection ports.BrokerPhysicalConnection, err error) *MockBrokerEndpointConnector_Connect_Call {
1120+ _c.Call.Return(brokerPhysicalConnection, err)
1121+ return _c
1122+}
1123+
1124+func (_c *MockBrokerEndpointConnector_Connect_Call) RunAndReturn(run func(ctx context.Context, endpoint string, policy ports.BrokerPolicy) (ports.BrokerPhysicalConnection, error)) *MockBrokerEndpointConnector_Connect_Call {
1125+ _c.Call.Return(run)
1126+ return _c
1127+}
1128+
1129+// NewMockBrokerListener creates a new instance of MockBrokerListener. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
1130+// The first argument is typically a *testing.T value.
1131+func NewMockBrokerListener(t interface {
1132+ mock.TestingT
1133+ Cleanup(func())
1134+}) *MockBrokerListener {
1135+ mock := &MockBrokerListener{}
1136+ mock.Mock.Test(t)
1137+
1138+ t.Cleanup(func() { mock.AssertExpectations(t) })
1139+
1140+ return mock
1141+}
1142+
1143+// MockBrokerListener is an autogenerated mock type for the BrokerListener type
1144+type MockBrokerListener struct {
1145+ mock.Mock
1146+}
1147+
1148+type MockBrokerListener_Expecter struct {
1149+ mock *mock.Mock
1150+}
1151+
1152+func (_m *MockBrokerListener) EXPECT() *MockBrokerListener_Expecter {
1153+ return &MockBrokerListener_Expecter{mock: &_m.Mock}
1154+}
1155+
1156+// Accept provides a mock function for the type MockBrokerListener
1157+func (_mock *MockBrokerListener) Accept() (ports.BrokerService, error) {
1158+ ret := _mock.Called()
1159+
1160+ if len(ret) == 0 {
1161+ panic("no return value specified for Accept")
1162+ }
1163+
1164+ var r0 ports.BrokerService
1165+ var r1 error
1166+ if returnFunc, ok := ret.Get(0).(func() (ports.BrokerService, error)); ok {
1167+ return returnFunc()
1168+ }
1169+ if returnFunc, ok := ret.Get(0).(func() ports.BrokerService); ok {
1170+ r0 = returnFunc()
1171+ } else {
1172+ if ret.Get(0) != nil {
1173+ r0 = ret.Get(0).(ports.BrokerService)
1174+ }
1175+ }
1176+ if returnFunc, ok := ret.Get(1).(func() error); ok {
1177+ r1 = returnFunc()
1178+ } else {
1179+ r1 = ret.Error(1)
1180+ }
1181+ return r0, r1
1182+}
1183+
1184+// MockBrokerListener_Accept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Accept'
1185+type MockBrokerListener_Accept_Call struct {
1186+ *mock.Call
1187+}
1188+
1189+// Accept is a helper method to define mock.On call
1190+func (_e *MockBrokerListener_Expecter) Accept() *MockBrokerListener_Accept_Call {
1191+ return &MockBrokerListener_Accept_Call{Call: _e.mock.On("Accept")}
1192+}
1193+
1194+func (_c *MockBrokerListener_Accept_Call) Run(run func()) *MockBrokerListener_Accept_Call {
1195+ _c.Call.Run(func(args mock.Arguments) {
1196+ run()
1197+ })
1198+ return _c
1199+}
1200+
1201+func (_c *MockBrokerListener_Accept_Call) Return(brokerService ports.BrokerService, err error) *MockBrokerListener_Accept_Call {
1202+ _c.Call.Return(brokerService, err)
1203+ return _c
1204+}
1205+
1206+func (_c *MockBrokerListener_Accept_Call) RunAndReturn(run func() (ports.BrokerService, error)) *MockBrokerListener_Accept_Call {
1207+ _c.Call.Return(run)
1208+ return _c
1209+}
1210+
1211+// Addr provides a mock function for the type MockBrokerListener
1212+func (_mock *MockBrokerListener) Addr() string {
1213+ ret := _mock.Called()
1214+
1215+ if len(ret) == 0 {
1216+ panic("no return value specified for Addr")
1217+ }
1218+
1219+ var r0 string
1220+ if returnFunc, ok := ret.Get(0).(func() string); ok {
1221+ r0 = returnFunc()
1222+ } else {
1223+ r0 = ret.Get(0).(string)
1224+ }
1225+ return r0
1226+}
1227+
1228+// MockBrokerListener_Addr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Addr'
1229+type MockBrokerListener_Addr_Call struct {
1230+ *mock.Call
1231+}
1232+
1233+// Addr is a helper method to define mock.On call
1234+func (_e *MockBrokerListener_Expecter) Addr() *MockBrokerListener_Addr_Call {
1235+ return &MockBrokerListener_Addr_Call{Call: _e.mock.On("Addr")}
1236+}
1237+
1238+func (_c *MockBrokerListener_Addr_Call) Run(run func()) *MockBrokerListener_Addr_Call {
1239+ _c.Call.Run(func(args mock.Arguments) {
1240+ run()
1241+ })
1242+ return _c
1243+}
1244+
1245+func (_c *MockBrokerListener_Addr_Call) Return(s string) *MockBrokerListener_Addr_Call {
1246+ _c.Call.Return(s)
1247+ return _c
1248+}
1249+
1250+func (_c *MockBrokerListener_Addr_Call) RunAndReturn(run func() string) *MockBrokerListener_Addr_Call {
1251+ _c.Call.Return(run)
1252+ return _c
1253+}
1254+
1255+// Close provides a mock function for the type MockBrokerListener
1256+func (_mock *MockBrokerListener) Close() error {
1257+ ret := _mock.Called()
1258+
1259+ if len(ret) == 0 {
1260+ panic("no return value specified for Close")
1261+ }
1262+
1263+ var r0 error
1264+ if returnFunc, ok := ret.Get(0).(func() error); ok {
1265+ r0 = returnFunc()
1266+ } else {
1267+ r0 = ret.Error(0)
1268+ }
1269+ return r0
1270+}
1271+
1272+// MockBrokerListener_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
1273+type MockBrokerListener_Close_Call struct {
1274+ *mock.Call
1275+}
1276+
1277+// Close is a helper method to define mock.On call
1278+func (_e *MockBrokerListener_Expecter) Close() *MockBrokerListener_Close_Call {
1279+ return &MockBrokerListener_Close_Call{Call: _e.mock.On("Close")}
1280+}
1281+
1282+func (_c *MockBrokerListener_Close_Call) Run(run func()) *MockBrokerListener_Close_Call {
1283+ _c.Call.Run(func(args mock.Arguments) {
1284+ run()
1285+ })
1286+ return _c
1287+}
1288+
1289+func (_c *MockBrokerListener_Close_Call) Return(err error) *MockBrokerListener_Close_Call {
1290+ _c.Call.Return(err)
1291+ return _c
1292+}
1293+
1294+func (_c *MockBrokerListener_Close_Call) RunAndReturn(run func() error) *MockBrokerListener_Close_Call {
1295+ _c.Call.Return(run)
1296+ return _c
1297+}
A internal/usecase/broker/doc.go +7 -0
@@ -0,0 +1,7 @@
1+// Package broker implements vev's per-user connection broker.
2+//
3+// The broker is the machine-wide owner of connectivity: configured hosts,
4+// daemon reachability and catalogue snapshots, pooled physical transports,
5+// and multiplexed logical streams. It never owns picker UI, sessions,
6+// PTYs, terminal geometry, or attachment semantics. See ADR 001.
7+package broker
@@ -0,0 +1,7 @@
1+// Package broker implements vev's per-user connection broker.
2+//
3+// The broker is the machine-wide owner of connectivity: configured hosts,
4+// daemon reachability and catalogue snapshots, pooled physical transports,
5+// and multiplexed logical streams. It never owns picker UI, sessions,
6+// PTYs, terminal geometry, or attachment semantics. See ADR 001.
7+package broker

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