diff --git a/gns3server/compute/marker/marker_listener.py b/gns3server/compute/marker/marker_listener.py index 40cdd97b6..a30769c1e 100644 --- a/gns3server/compute/marker/marker_listener.py +++ b/gns3server/compute/marker/marker_listener.py @@ -114,13 +114,24 @@ class MarkerListener(asyncio.DatagramProtocol): # signals that carry no `link=`. signal_link = link if link and link != "-" else None + # Normalize the tag to int so the event matches the REST schema + # (MarkerCreate.tag is Optional[int]): the signal merely echoes the + # decimal we installed via `mark tag `, so parsing cannot + # fail for well-formed signals; a malformed value keeps the registered + # int, and the tag is None only when neither side carries one. + event_tag = registered_tag + if tag and tag != "-": + try: + event_tag = int(tag) + except ValueError: + pass # malformed signal tag: keep the registered value + event = { "project_id": project_id, "node_id": node_id, "link_id": signal_link or link_id, "filter": filter_name, - # Prefer the value carried in the signal; fall back to the one we registered. - "tag": tag if tag and tag != "-" else registered_tag, + "tag": event_tag, "ts": ts, "len": int(length) if length and length.isdigit() else 0, # Travel direction relative to the capture node (node_id above); diff --git a/tests/compute/marker/test_marker_manager.py b/tests/compute/marker/test_marker_manager.py index da495ae92..5917870d7 100644 --- a/tests/compute/marker/test_marker_manager.py +++ b/tests/compute/marker/test_marker_manager.py @@ -121,7 +121,11 @@ class TestMarkerListener: assert ev["node_id"] == "n1" assert ev["link_id"] == "l1" assert ev["filter"] == "f1" - assert ev["tag"] == "7" + # The event tag is normalized to int to match the REST schema — the + # signal echoes the decimal we installed, so str and int variants of + # the same tag must never reach consumers as different keys. + assert ev["tag"] == 7 + assert isinstance(ev["tag"], int) assert ev["ts"] == pytest.approx(1700000000.123456) assert ev["len"] == 98 # No dir= in the signal (legacy uBridge) → undirected. @@ -163,6 +167,24 @@ class TestMarkerListener: lis.datagram_received(b"MARK 2.0 node=n filter=f tag=- len=20\n", None) assert fmgr.events[0][1]["tag"] == 42 + def test_non_numeric_signal_tag_falls_back_to_registered(self): + # A corrupt/unknown signal value must not leak a str tag into the + # event stream — the registry's int wins. + fmgr = FakeMarkerManager() + fmgr.register("p", "n", "f", "l", tag=42) + lis = MarkerListener(fmgr) + lis.connection_made(None) + lis.datagram_received(b"MARK 2.0 node=n filter=f tag=oops len=20\n", None) + assert fmgr.events[0][1]["tag"] == 42 + + def test_no_tag_anywhere_is_none(self): + fmgr = FakeMarkerManager() + fmgr.register("p", "n", "f", "l", tag=None) + lis = MarkerListener(fmgr) + lis.connection_made(None) + lis.datagram_received(b"MARK 2.0 node=n filter=f tag=- len=20\n", None) + assert fmgr.events[0][1]["tag"] is None + def test_link_in_signal_overrides_registry_link(self): # Per-link attribution (contract §3.2/§3.3): the signal's `link=` is # authoritative and must disambiguate links sharing a node+filter —