From c9f2a03064291cc9ae26b986c42e1177652982dc Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Tue, 3 Nov 2026 18:04:03 +0000 Subject: [PATCH 1/2] fix(reporting): allocate a unique handler name for every installation Give each newly installed reporting handler its own UUID-backed class name. Service addresses can be reused, and one service can install sellers with separate advertised tool sets. Preserve same-platform reinstall identity and strict conflicting handler registration checks. Fixes #1846 --- src/adcp/reporting/service.py | 4 +- tests/test_reporting_install_identity.py | 63 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/test_reporting_install_identity.py diff --git a/src/adcp/reporting/service.py b/src/adcp/reporting/service.py index ba973cc65f..1563f3abad 100644 --- a/src/adcp/reporting/service.py +++ b/src/adcp/reporting/service.py @@ -18,6 +18,7 @@ from datetime import datetime, timedelta, timezone from types import MappingProxyType from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable +from uuid import uuid4 from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from pydantic import BaseModel @@ -1430,7 +1431,8 @@ async def delegated(instance: Any, *args: Any, **kwargs: Any) -> Any: attrs[name] = passthrough(name, method) - installed_name = f"ReliableReporting{original.__name__}_{id(self):x}" + # Registrations outlive services, and one service can install several sellers. + installed_name = f"ReliableReporting{original.__name__}_{uuid4().hex}" installed = type( installed_name, (ReportingInstallMixin, original), diff --git a/tests/test_reporting_install_identity.py b/tests/test_reporting_install_identity.py new file mode 100644 index 0000000000..2859d3c76e --- /dev/null +++ b/tests/test_reporting_install_identity.py @@ -0,0 +1,63 @@ +"""Reporting installations must retain each seller's own advertised tools.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from adcp.reporting.service import ReliableReportingService +from adcp.server import ADCPHandler +from adcp.server.mcp_tools import get_tools_for_handler, register_handler_tools + + +class Seller(ADCPHandler): + def __init__(self, tool: str) -> None: + super().__init__() + self.tool = tool + + def advertised_tools_for_instance(self) -> set[str]: + return {self.tool} + + async def get_products(self, params: Any, context: Any = None) -> Any: + return {"products": [{"product_id": "first-seller"}]} + + async def get_media_buys(self, params: Any, context: Any = None) -> Any: + return {"media_buys": [{"media_buy_id": "second-seller"}]} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("same_service", [True, False]) +async def test_install_preserves_distinct_seller_surfaces(same_service: bool) -> None: + first_service = ReliableReportingService.memory(account_context=lambda *_: None) + second_service = ( + first_service + if same_service + else ReliableReportingService.memory(account_context=lambda *_: None) + ) + first = first_service.install(Seller("get_products")) + first_type = type(first) + assert first_service.install(first) is first + assert type(first) is first_type + + second = second_service.install(Seller("get_media_buys")) + second_type = type(second) + assert second_type.__name__ != first_type.__name__ + assert second_service.install(second) is second + assert type(second) is second_type + + first_tools = {item["name"] for item in get_tools_for_handler(first, _include_schemas=False)} + second_tools = {item["name"] for item in get_tools_for_handler(second, _include_schemas=False)} + assert "get_products" in first_tools and "get_media_buys" not in first_tools + assert "get_media_buys" in second_tools and "get_products" not in second_tools + assert await first.get_products({}) == {"products": [{"product_id": "first-seller"}]} + assert await second.get_media_buys({}) == {"media_buys": [{"media_buy_id": "second-seller"}]} + + # New installation names must not weaken the public registry's conflict guard. + registered = first_type.advertised_tools + register_handler_tools(first_type.__name__, registered) + with pytest.raises(ValueError, match="called twice with different tool sets"): + register_handler_tools(first_type.__name__, registered | {"get_media_buys"}) + assert { + item["name"] for item in get_tools_for_handler(first, _include_schemas=False) + } == first_tools From afff251c264695cc3ed8ccff7a7929ed27a5a47b Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Tue, 3 Nov 2026 18:27:12 +0000 Subject: [PATCH 2/2] test(reporting): isolate the installation fixture from global handler names --- tests/test_reporting_install_identity.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_reporting_install_identity.py b/tests/test_reporting_install_identity.py index 2859d3c76e..073cbc61fc 100644 --- a/tests/test_reporting_install_identity.py +++ b/tests/test_reporting_install_identity.py @@ -11,7 +11,8 @@ from adcp.server.mcp_tools import get_tools_for_handler, register_handler_tools -class Seller(ADCPHandler): +# Raw handler names share a process-wide registry with other tests. +class _InstallationIdentitySeller(ADCPHandler): def __init__(self, tool: str) -> None: super().__init__() self.tool = tool @@ -35,12 +36,12 @@ async def test_install_preserves_distinct_seller_surfaces(same_service: bool) -> if same_service else ReliableReportingService.memory(account_context=lambda *_: None) ) - first = first_service.install(Seller("get_products")) + first = first_service.install(_InstallationIdentitySeller("get_products")) first_type = type(first) assert first_service.install(first) is first assert type(first) is first_type - second = second_service.install(Seller("get_media_buys")) + second = second_service.install(_InstallationIdentitySeller("get_media_buys")) second_type = type(second) assert second_type.__name__ != first_type.__name__ assert second_service.install(second) is second