Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/adcp/reporting/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
64 changes: 64 additions & 0 deletions tests/test_reporting_install_identity.py
Original file line number Diff line number Diff line change
@@ -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
Loading