@@ -55,6 +55,20 @@ def _make_remote_publication(sid: str) -> rtc.RemoteTrackPublication:
5555 return pub
5656
5757
58+ def _make_local_participant (identity : str ) -> rtc .LocalParticipant :
59+ p = rtc .LocalParticipant .__new__ (rtc .LocalParticipant )
60+ p ._info = proto_participant .ParticipantInfo (identity = identity )
61+ p ._track_publications = {}
62+ return p
63+
64+
65+ def _make_local_publication (sid : str ) -> rtc .LocalTrackPublication :
66+ pub = rtc .LocalTrackPublication .__new__ (rtc .LocalTrackPublication )
67+ pub ._info = proto_track .TrackPublicationInfo (sid = sid )
68+ pub ._track = None
69+ return pub
70+
71+
5872def _make_track (sid : str = "TR_x" ) -> rtc .Track :
5973 track = rtc .Track .__new__ (rtc .Track )
6074 track ._info = proto_track .TrackInfo (sid = sid )
@@ -468,3 +482,66 @@ async def test_aclose_leaves_processor_open_when_auto_close_false() -> None:
468482 await stream .aclose ()
469483
470484 assert processor .close_calls == 0
485+
486+
487+ # -- local_track_republished regression ---------------------------------------
488+
489+
490+ def test_local_track_republished_updates_track_sid_and_repushes_metadata () -> None :
491+ """A full-reconnect republish re-issues the publication SID. The handler must
492+ keep the local-track invariant (track.sid == publication.sid) intact and
493+ re-push metadata so attached processors learn the new SID — otherwise the
494+ SID-based lookup in Track._push_processor_metadata_to_stream fails and the
495+ processor receives empty participant_identity / publication_sid.
496+ """
497+ room = _make_room (name = "room-1" , token = "tok-1" , url = "wss://r" )
498+ local = _make_local_participant ("agent" )
499+ room ._local_participant = local
500+
501+ # Local track published under the OLD sid (mirrors publish_track:
502+ # track.sid == publication.sid).
503+ track = _make_track (sid = "OLD" )
504+ publication = _make_local_publication (sid = "OLD" )
505+ publication ._track = track
506+ local ._track_publications ["OLD" ] = publication
507+ track ._set_room (room )
508+
509+ # Processor attached before the reconnect sees the OLD sid.
510+ processor = _RecordingProcessor ()
511+ _stream = _make_stream (track = track , processor = processor ) # noqa: F841
512+ assert processor .stream_info_calls [- 1 ] == {
513+ "room_name" : "room-1" ,
514+ "participant_identity" : "agent" ,
515+ "publication_sid" : "OLD" ,
516+ }
517+
518+ # Dispatch a synthetic local_track_republished re-issuing the sid as NEW.
519+ event = proto_room .RoomEvent (
520+ local_track_republished = proto_room .LocalTrackRepublished (
521+ previous_sid = "OLD" ,
522+ info = proto_track .TrackPublicationInfo (sid = "NEW" ),
523+ )
524+ )
525+ room ._on_room_event (event )
526+
527+ # Invariant restored + dict rekeyed.
528+ assert track .sid == "NEW"
529+ assert "NEW" in local ._track_publications
530+ assert "OLD" not in local ._track_publications
531+
532+ # Existing attached processor was re-pushed with the NEW sid (non-empty).
533+ assert processor .stream_info_calls [- 1 ] == {
534+ "room_name" : "room-1" ,
535+ "participant_identity" : "agent" ,
536+ "publication_sid" : "NEW" ,
537+ }
538+
539+ # Regression guard: a stream created AFTER republish also resolves NEW
540+ # (the exact path from the bug report — stale track.sid would yield "").
541+ processor2 = _RecordingProcessor ()
542+ _stream2 = _make_stream (track = track , processor = processor2 ) # noqa: F841
543+ assert processor2 .stream_info_calls [- 1 ] == {
544+ "room_name" : "room-1" ,
545+ "participant_identity" : "agent" ,
546+ "publication_sid" : "NEW" ,
547+ }
0 commit comments