Ctrl K
ComunidadGuías…

test/network/test_channel_pruning.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Copyright (c) 2026, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0

"""Per-channel cache pruning on terminal channel transitions.

The hub holds several per-channel scratch maps — ``_adapter_states``,
``_channel_locks``, ``_channel_open_waiters``, ``_fired_violations``.
They must be pruned when a channel transitions to a terminal state
(``CLOSED`` / ``EXPIRED``) so a long-lived hub processing many short
channels does not grow without bound.

The channel metadata itself (``_channels``) is intentionally kept for
auditability + ``read_wal()`` access. Only volatile per-channel
caches are pruned.
"""

import asyncio

import pytest

from ag2 import Agent
from ag2.knowledge import MemoryKnowledgeStore
from ag2.network import (
    Hub,
)
from ag2.testing import TestConfig


def _agent(name: str) -> Agent:
    return Agent(name=name, config=TestConfig())


@pytest.mark.asyncio
async def test_adapter_state_retained_after_close_for_analysis() -> None:
    """``_adapter_states`` is intentionally kept post-close for inspection.

    Fold state carries analytical value for tests, debug tools, and
    post-mortem analysis. Pruning the heavier per-channel
    synchronization primitives (locks) is enough to bound memory; the
    state dataclass itself is small.
    """
    store = MemoryKnowledgeStore()
    hub = await Hub.open(store, ttl_sweep_interval=0)
    alice = await hub.register(_agent("alice"))
    await hub.register(_agent("bob"))
    channel = await alice.open(type="conversation", target="bob")
    channel_id = channel.channel_id

    pre_close_state = hub.adapter_state(channel_id)
    assert pre_close_state is not None

    await hub.close_channel(channel_id, reason="explicit")

    # State stays available for analysis.
    assert hub.adapter_state(channel_id) is pre_close_state
    # Channel metadata stays for audit / WAL access.
    metadata = await hub.get_channel(channel_id)
    assert metadata.is_terminal()

    await hub.close()


@pytest.mark.asyncio
async def test_channel_locks_pruned_on_close() -> None:
    """``_channel_locks`` drops the entry on terminal transition."""
    store = MemoryKnowledgeStore()
    hub = await Hub.open(store, ttl_sweep_interval=0)
    alice = await hub.register(_agent("alice"))
    await hub.register(_agent("bob"))
    channel = await alice.open(type="conversation", target="bob")
    channel_id = channel.channel_id

    # WAL append acquired the lock, so it's in the dict.
    await channel.send("hello")
    assert channel_id in hub._channel_locks  # pre-close

    await hub.close_channel(channel_id, reason="explicit")
    assert channel_id not in hub._channel_locks  # post-close

    await hub.close()


@pytest.mark.asyncio
async def test_fired_violations_pruned_on_close() -> None:
    """``_fired_violations`` (already pre-existing prune) keeps the contract."""
    store = MemoryKnowledgeStore()
    hub = await Hub.open(store, ttl_sweep_interval=0)
    alice = await hub.register(_agent("alice"))
    await hub.register(_agent("bob"))
    channel = await alice.open(type="conversation", target="bob")
    channel_id = channel.channel_id

    # Force a fired-violations entry directly (no live expectation).
    hub._fired_violations[channel_id] = {(0, "max_silence", "")}
    await hub.close_channel(channel_id, reason="explicit")
    assert channel_id not in hub._fired_violations

    await hub.close()


@pytest.mark.asyncio
async def test_pruning_under_many_short_channels_keeps_caches_bounded() -> None:
    """Open + close 20 channels — the per-channel caches stay bounded.

    Realistic proxy for the long-lived-hub case: every channel that
    closes must release its per-channel scratch state so the dicts
    don't grow without bound.
    """
    store = MemoryKnowledgeStore()
    hub = await Hub.open(store, ttl_sweep_interval=0)
    alice = await hub.register(_agent("alice"))
    await hub.register(_agent("bob"))

    for _ in range(20):
        channel = await alice.open(type="conversation", target="bob")
        await channel.send("ping")
        await hub.close_channel(channel.channel_id, reason="cycle")

    # Yield so any sweeper / dispatch task settles.
    await asyncio.sleep(0.1)

    # All 20 closed → the heavy per-channel synchronization caches
    # are empty. ``_adapter_states`` is retained by design.
    assert hub._channel_locks == {}
    assert hub._fired_violations == {}
    assert hub._channel_open_waiters == {}

    await hub.close()