11from __future__ import annotations
22
3+ import asyncio
4+ from dataclasses import dataclass , field
35from enum import Enum
46from typing import Any , Mapping
57
@@ -181,6 +183,138 @@ def build_matrix_row(
181183 return row
182184
183185
186+ @dataclass
187+ class ClientSession :
188+ client_id : str
189+ connection_id : str
190+ session_id : str
191+ closed : bool = False
192+ payloads : list [str ] = field (default_factory = list )
193+
194+
195+ class ClientSessionTopologyHarness :
196+ """Runtime-facing carrier topology recorder for governed proof rows."""
197+
198+ def __init__ (self , carrier : ProtocolCarrier , scope : SessionScope | None = None ) -> None :
199+ self .carrier = carrier
200+ self .scope = scope
201+ self .sessions : dict [str , ClientSession ] = {}
202+ self .events : list [dict [str , Any ]] = []
203+
204+ def open (self , client_id : str , connection_id : str , session_id : str , topology : ClientTopology ) -> None :
205+ self .sessions [session_id ] = ClientSession (
206+ client_id = client_id ,
207+ connection_id = connection_id ,
208+ session_id = session_id ,
209+ )
210+ self .events .append (self .record ("open" , client_id , connection_id , session_id , topology ))
211+
212+ def send (
213+ self ,
214+ client_id : str ,
215+ connection_id : str ,
216+ session_id : str ,
217+ topology : ClientTopology ,
218+ payload : str ,
219+ ** identifiers : Any ,
220+ ) -> None :
221+ session = self .session_for (client_id , connection_id , session_id )
222+ session .payloads .append (payload )
223+ self .events .append (
224+ self .record (
225+ "send" ,
226+ client_id ,
227+ connection_id ,
228+ session_id ,
229+ topology ,
230+ payload = payload ,
231+ ** identifiers ,
232+ )
233+ )
234+
235+ async def send_async (
236+ self ,
237+ client_id : str ,
238+ connection_id : str ,
239+ session_id : str ,
240+ topology : ClientTopology ,
241+ payload : str ,
242+ delay : float = 0.0 ,
243+ ** identifiers : Any ,
244+ ) -> None :
245+ await asyncio .sleep (delay )
246+ self .send (client_id , connection_id , session_id , topology , payload , ** identifiers )
247+
248+ def close (self , client_id : str , connection_id : str , session_id : str , topology : ClientTopology ) -> None :
249+ session = self .session_for (client_id , connection_id , session_id )
250+ session .closed = True
251+ self .events .append (self .record ("close" , client_id , connection_id , session_id , topology ))
252+
253+ def record (
254+ self ,
255+ subevent : str ,
256+ client_id : str ,
257+ connection_id : str ,
258+ session_id : str ,
259+ topology : ClientTopology ,
260+ ** extra : Any ,
261+ ) -> dict [str , Any ]:
262+ return build_matrix_row (
263+ protocol_carrier = self .carrier ,
264+ client_topology = topology ,
265+ session_scope = self .scope ,
266+ disposition = CoverageDisposition .COVERED ,
267+ lifecycle_behavior = CoverageDisposition .COVERED ,
268+ identity_isolation = CoverageDisposition .COVERED ,
269+ ordering_behavior = CoverageDisposition .COVERED ,
270+ pressure_mode = CoverageDisposition .REQUIRED ,
271+ fault_mode = CoverageDisposition .REQUIRED ,
272+ client_id = client_id ,
273+ connection_id = connection_id ,
274+ session_id = session_id ,
275+ subevent = subevent ,
276+ ** extra ,
277+ )
278+
279+ def session_for (self , client_id : str , connection_id : str , session_id : str ) -> ClientSession :
280+ session = self .sessions [session_id ]
281+ if session .client_id != client_id or session .connection_id != connection_id :
282+ raise PermissionError ("cross-client or cross-connection session access rejected" )
283+ if session .closed :
284+ raise RuntimeError ("post-close send rejected" )
285+ return session
286+
287+
288+ def sequential_pair (carrier : ProtocolCarrier , scope : SessionScope | None = None ) -> ClientSessionTopologyHarness :
289+ topology = ClientTopology .SEQUENTIAL_CLIENTS
290+ harness = ClientSessionTopologyHarness (carrier , scope )
291+ harness .open ("client-a" , "conn-a" , "session-a" , topology )
292+ harness .send ("client-a" , "conn-a" , "session-a" , topology , "a-1" )
293+ harness .close ("client-a" , "conn-a" , "session-a" , topology )
294+ harness .open ("client-b" , "conn-b" , "session-b" , topology )
295+ harness .send ("client-b" , "conn-b" , "session-b" , topology , "b-1" )
296+ harness .close ("client-b" , "conn-b" , "session-b" , topology )
297+ return harness
298+
299+
300+ def bounded_interleaved_pair (
301+ carrier : ProtocolCarrier ,
302+ scope : SessionScope | None = None ,
303+ ) -> ClientSessionTopologyHarness :
304+ topology = ClientTopology .BOUNDED_INTERLEAVED_CLIENTS
305+ harness = ClientSessionTopologyHarness (carrier , scope )
306+ harness .open ("client-a" , "conn-a" , "session-a" , topology )
307+ harness .open ("client-b" , "conn-b" , "session-b" , topology )
308+ for client_id , connection_id , session_id , payload in (
309+ ("client-a" , "conn-a" , "session-a" , "a-1" ),
310+ ("client-b" , "conn-b" , "session-b" , "b-1" ),
311+ ("client-a" , "conn-a" , "session-a" , "a-2" ),
312+ ("client-b" , "conn-b" , "session-b" , "b-2" ),
313+ ):
314+ harness .send (client_id , connection_id , session_id , topology , payload )
315+ return harness
316+
317+
184318__all__ = [
185319 "BEHAVIOR_AXIS_VALUES" ,
186320 "CLIENT_TOPOLOGY_VALUES" ,
@@ -192,12 +326,16 @@ def build_matrix_row(
192326 "REQUIRED_MATRIX_AXES" ,
193327 "SESSION_SCOPE_VALUES" ,
194328 "BehaviorAxis" ,
329+ "ClientSession" ,
330+ "ClientSessionTopologyHarness" ,
195331 "ClientTopology" ,
196332 "CoverageDisposition" ,
197333 "ProtocolCarrier" ,
198334 "SessionScope" ,
199335 "build_matrix_row" ,
336+ "bounded_interleaved_pair" ,
200337 "classify_default_session_scope" ,
338+ "sequential_pair" ,
201339 "validate_governed_identifiers" ,
202340 "validate_matrix_row" ,
203341 "validate_no_internal_lane" ,
0 commit comments