Ctrl K
ComunidadGuías…

test/AGENTS.md

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

Archivo original ↗Preguntar a KiBot
# test/ Guidelines

Use `just test` as alias for `pytest` execution to run the tests.

## Testing Conventions

Always write public API-based tests. Do not assert implementation details.

```python
# === BAD - digging into implementation details ===
async def test_collects_events_in_window(self) -> None:
    stream = MemoryStream()
    ctx = Context(stream=stream)
    batches: list = []

    async def callback(events, _ctx):
        batches.append(events)

    watch = CadenceWatch(max_wait=0.1, condition=ToolCallEvent)
    watch.arm(stream, callback)

    await stream.send(ToolCallEvent(name="t1", arguments="{}"), ctx)
    await stream.send(ToolCallEvent(name="t2", arguments="{}"), ctx)

    await asyncio.sleep(0.2)

    assert len(batches) == 1
    assert len(batches[0]) == 2


# === GOOD - public-api based test ===
async def test_collects_events_in_window(self) -> None:
    # arrange stream
    stream = MemoryStream()
    batches: list[BaseEvent] = []

    async def callback(events: BaseEvent, ctx: Context) -> None:
        batches.append(events)

    watch = CadenceWatch(max_wait=0.01, condition=ToolCallEvent)
    watch.arm(stream, callback)

    # arrange agent
    tool_calls = [
        ToolCallEvent(name="t1", arguments="{}"),
        ToolCallEvent(name="t2", arguments="{}"),
    ]

    agent = Agent(
        "test-agent",
        config=testing.TestConfig(tool_calls, "Done"),
    )

    @agent.tool
    def t1(): ...
    @agent.tool
    def t2(): ...

    # act
    await agent.ask("Hello, world!", stream=stream)
    await asyncio.sleep(0.02)

    # assert
    assert batches == [
        IsList(*tool_calls, check_order=False),
    ]
```

- Always use `ag2.testing.TestConfig` to mock LLM responses in agent-based tests.
- Always use `ag2.testing.TrackingConfig` to validate messages the framework sends to the LLM (for example: tool results and user input).

### No monkeypatching internals

Do not use `monkeypatch.setattr`, `setattr` on an instance, or `unittest.mock.patch` to swap out a private function, method, or attribute (anything `_prefixed`). Patching internals pins the test to *how* the code works today, so it keeps passing even after the real behavior breaks.

The agent's public seam is `config=`. Script the LLM's turns with `ag2.testing.TestConfig` instead of reaching for the agent's private client — the same test, before and after:

```python
from ag2 import Agent
from ag2.events import ToolCallEvent
from ag2.testing import TestConfig


# BAD — patch the agent's private client to script the turn. The test breaks the
# moment that internal is renamed, and a green run proves nothing about the wiring.
async def test_agent_answers_with_tool(monkeypatch):
    agent = Agent("weather", tools=[get_weather])
    monkeypatch.setattr(agent, "_client", _scripted_client(...))  # private attribute
    ...


# GOOD — script the same turns through the public `config=` seam.
async def test_agent_answers_with_tool():
    agent = Agent(
        "weather",
        config=TestConfig(
            ToolCallEvent(name="get_weather", arguments='{"city": "Tokyo"}'),  # turn 1: call the tool
            "It's sunny in Tokyo.",  # turn 2: final reply
        ),
        tools=[get_weather],
    )

    reply = await agent.ask("Weather in Tokyo?")

    assert reply.body == "It's sunny in Tokyo."
```

Use `TrackingConfig` (which wraps a `TestConfig`) when you also need to assert what the framework *sent* the LLM — again, no patching required.

Failure paths follow the same principle: raise from a public collaborator, never a patched internal — an `Agent` double whose `ask` raises, a `TraceSource` whose `load` raises, or a `LinkEndpoint` whose `frames()` raises (registered with `hub.attach_endpoint(...)`). If a branch can *only* be reached by patching an internal, it isn't publicly observable: cover the observable contract instead of reaching in to hit the line.

### Assertion style

Avoid chained field-access assertions like `result[0]["tool_calls"][0]["function"]["arguments"] == {...}`. Instead, compare the whole object directly (`assert msg == {...}`) or use **dirty-equals** `IsPartialDict` when only some fields matter:

```python
# Bad
assert result[0]["role"] == "assistant"
assert result[0]["tool_calls"][0]["function"]["arguments"] == {}

# Good — full comparison
assert result[0] == {"role": "assistant", "tool_calls": [...]}

# Good — partial match with dirty-equals (always use dict syntax, not kwargs)
from dirty_equals import IsPartialDict

assert result[0] == IsPartialDict({"role": "assistant"})  # Good
assert result[0] == IsPartialDict(role="assistant")  # Bad — use dict syntax
```

### Imports

All imports must be at the top of the test file. Never place imports inside individual test functions until user asks for it.

### Function vs class-based tests

Use **plain functions** for standalone tests. Use **classes** to group multiple related tests that share a logical subject (e.g., `TestImageUrlInput`, `TestBinaryInput`). Do not wrap a single test method in a class — keep it a plain function instead.

If you need to apply markers to each test in class, apply them to the class itself.

```python
# Bad - markers are applied to each test individually
class TestAgent:
    @pytest.mark.asyncio
    async def test_defaults(self, context: Context) -> None: ...

    @pytest.mark.asyncio
    async def test_defaults(self, context: Context) -> None: ...


# Good - markers are applied to the class itself
@pytest.mark.asyncio
class TestAgent:
    async def test_defaults(self, context: Context) -> None: ...

    async def test_defaults(self, context: Context) -> None: ...
```

### Section comments

Do not use banner-style section dividers (e.g. `# ---\n# Section\n# ---`). Class names and test names are sufficient structure.

## Builtin Tools Testing

### Structure

Provider-specific tool tests live in `test/config/{provider}/tools/`:
- `test_{tool}.py` — e2e tests for supported tools (one file per tool)
- `test_unsupported.py` — all unsupported tools for the provider in one file
- `test_tool_to_api.py` — generic function tool mapping (not builtin-specific)

Variable resolution tests live in `test/tools/test_resolve.py`.

Provider test packages must stay importable when their SDK is absent — the LLM
CI matrix installs one provider at a time, and an unguarded top-level
`import anthropic` (directly, or via `ag2.config.{provider}`) breaks *collection*
for the whole run, even though the test itself would be deselected.
`test/config/{provider}/__init__.py` already guards the package with
`pytest.importorskip(...)`, so put provider tests inside that package rather than
in a new directory. A test that spans providers (e.g. under `test/tools/`) must
guard itself at the top of the module, before the imports:

```python
import pytest

pytest.importorskip("anthropic")
pytest.importorskip("openai")

from ag2.config.anthropic.mappers import tool_to_api
```

### Test Pattern

Tests must be e2e: instantiate the **Tool** class, call `schemas()`, pass through the provider mapper:

```python
@pytest.mark.asyncio
async def test_defaults(context: Context) -> None:
    tool = WebSearchTool()

    [schema] = await tool.schemas(context)

    assert tool_to_api(schema) == {"type": "web_search_20250305", "name": "web_search"}
```

Do **not** instantiate schema classes directly in provider tests — always go through the Tool.

### Fixtures

Use the shared `context` pytest fixture from `test/config/conftest.py` (no need to import — pytest discovers it automatically):

```python
async def test_defaults(context: Context) -> None: ...
```

### Coverage Requirements

Every builtin tool must be tested in **every** provider:
- **Supported**: test the happy-path mapping in `test_{tool}.py`
- **Unsupported**: test `UnsupportedToolError` is raised in `test_unsupported.py`

For OpenAI, test both `tool_to_api` (completions) and `tool_to_responses_api` (responses) paths. Group unsupported tests under `TestCompletionsApi` / `TestResponsesApi` classes.

### Variable Resolution

Each tool that accepts `Variable` parameters needs exactly 2 tests in `test/tools/test_resolve.py`:
1. Value resolved from context
2. Missing key raises `KeyError`