From ec1bc5a33c690c33bc1f6ede76da99d2258c646f Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Tue, 3 Nov 2026 14:17:57 -0500 Subject: [PATCH] fix(reporting): isolate handler registrations for each installation * 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 * test(reporting): isolate the installation fixture from global handler names (cherry picked from commit 86302d09404067410a60d3f9d0f850790038a36f) --- src/adcp/reporting/service.py | 4 +- tests/test_reporting_install_identity.py | 64 ++++++++++++++++++++++++ 2 files changed, 67 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 e613f2282c..302ae45326 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 @@ -2086,7 +2087,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..073cbc61fc --- /dev/null +++ b/tests/test_reporting_install_identity.py @@ -0,0 +1,64 @@ +"""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 + + +# 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 + + 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(_InstallationIdentitySeller("get_products")) + first_type = type(first) + assert first_service.install(first) is first + assert type(first) is first_type + + 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 + 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