Ctrl K
ComunidadGuías…

test/acp/test_mappers.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

import base64
import logging
from typing import Any

import pytest
from acp import schema
from dirty_equals import IsPartialDict

from ag2.acp.events import ACPAvailableCommands, ACPModeChange, ACPPlan, ACPPlanEntry
from ag2.acp.mappers import (
    content_blocks_to_files,
    content_blocks_to_text,
    map_session_update,
    map_usage,
)
from ag2.acp.types import SessionUpdate
from ag2.events import ModelMessageChunk, ModelReasoning
from ag2.events.tool_events import BuiltinToolCallEvent, BuiltinToolResultEvent
from ag2.events.types import Usage


def _text(text: str) -> schema.TextContentBlock:
    return schema.TextContentBlock(type="text", text=text)


def test_message_chunk() -> None:
    ev = map_session_update(schema.AgentMessageChunk(session_update="agent_message_chunk", content=_text("hello")))
    assert isinstance(ev, ModelMessageChunk)
    assert ev.content == "hello"


def test_thought_chunk() -> None:
    ev = map_session_update(schema.AgentThoughtChunk(session_update="agent_thought_chunk", content=_text("thinking")))
    assert isinstance(ev, ModelReasoning)
    assert ev.content == "thinking"


def test_tool_call_start() -> None:
    ev = map_session_update(
        schema.ToolCallStart(
            session_update="tool_call",
            tool_call_id="tc1",
            title="Edit",
            raw_input={"path": "a.py"},
            status="pending",
        )
    )
    assert isinstance(ev, BuiltinToolCallEvent)
    assert ev.id == "tc1"
    assert ev.name == "Edit"
    assert ev.serialized_arguments == {"path": "a.py"}


def test_tool_call_progress() -> None:
    ev = map_session_update(
        schema.ToolCallProgress(
            session_update="tool_call_update",
            tool_call_id="tc1",
            title="Edit",
            status="completed",
            content=[schema.ContentToolCallContent(type="content", content=_text("done"))],
        )
    )
    assert isinstance(ev, BuiltinToolResultEvent)
    assert ev.parent_id == "tc1"


def test_plan() -> None:
    plan = map_session_update(
        schema.AgentPlanUpdate(
            session_update="plan",
            entries=[schema.PlanEntry(content="do x", status="pending", priority="high")],
        )
    )
    assert isinstance(plan, ACPPlan)
    assert plan.entries == [ACPPlanEntry(content="do x", status="pending", priority="high")]


def test_incremental_plan_items_update_maps_to_plan() -> None:
    """A `plan_update` carrying items is the same information as `plan`."""
    plan = map_session_update(
        schema.AgentPlanContentUpdate(
            session_update="plan_update",
            plan=schema.PlanUpdateItems(
                type="items",
                plan_id="p1",
                entries=[schema.PlanEntry(content="do y", status="in_progress", priority="low")],
            ),
        )
    )
    assert isinstance(plan, ACPPlan)
    assert plan.entries == [ACPPlanEntry(content="do y", status="in_progress", priority="low")]


@pytest.mark.parametrize(
    "update",
    [
        schema.AgentPlanContentUpdate(
            session_update="plan_update", plan=schema.PlanUpdateFile(type="file", plan_id="p1", uri="file:///plan.md")
        ),
        schema.AgentPlanContentUpdate(
            session_update="plan_update",
            plan=schema.PlanUpdateMarkdown(type="markdown", plan_id="p1", content="# plan"),
        ),
        schema.AgentPlanRemovedUpdate(session_update="plan_removed", plan_id="p1"),
    ],
)
def test_plan_updates_without_an_event_are_logged_not_silent(update: SessionUpdate, caplog: Any) -> None:
    """These carry no ACPPlan-shaped payload; ignoring them must at least be visible."""
    with caplog.at_level(logging.DEBUG, logger="ag2.acp.mappers"):
        assert map_session_update(update) is None
    assert "no AG2 event for ACP session update" in caplog.text


def test_usage_update_is_not_logged_as_unmapped(caplog: Any) -> None:
    """usage_update is consumed by map_usage by design, so it is not a gap."""
    with caplog.at_level(logging.DEBUG, logger="ag2.acp.mappers"):
        assert map_session_update(schema.UsageUpdate(session_update="usage_update", used=10, size=100)) is None
    assert caplog.text == ""


def test_mode_and_commands() -> None:
    mode = map_session_update(schema.CurrentModeUpdate(session_update="current_mode_update", current_mode_id="edit"))
    assert isinstance(mode, ACPModeChange) and mode.mode_id == "edit"

    cmds = map_session_update(
        schema.AvailableCommandsUpdate(
            session_update="available_commands_update",
            available_commands=[schema.AvailableCommand(name="/test", description="run")],
        )
    )
    assert isinstance(cmds, ACPAvailableCommands) and cmds.commands == ["/test"]


def test_unhandled_updates_return_none() -> None:
    assert map_session_update(schema.SessionInfoUpdate(session_update="session_info_update", title="x")) is None
    assert map_session_update(schema.UsageUpdate(session_update="usage_update", size=1, used=1)) is None
    assert map_session_update(schema.UserMessageChunk(session_update="user_message_chunk", content=_text("hi"))) is None


def test_content_blocks_to_text_concatenates() -> None:
    text = content_blocks_to_text([
        _text("a"),
        schema.ImageContentBlock(type="image", data="x", mime_type="image/png"),
        _text("b"),
    ])
    assert text == "ab"


def test_content_blocks_to_files_decodes_image() -> None:
    data = base64.b64encode(b"img").decode()
    files = content_blocks_to_files([schema.ImageContentBlock(type="image", data=data, mime_type="image/png")])
    assert files[0].data == b"img"
    assert files[0].metadata == IsPartialDict({"mimeType": "image/png"})


def test_map_usage() -> None:
    usage = map_usage(
        schema.Usage(
            input_tokens=10,
            output_tokens=5,
            total_tokens=15,
            cached_read_tokens=2,
            thought_tokens=3,
        )
    )
    assert usage == Usage(
        prompt_tokens=10,
        completion_tokens=5,
        total_tokens=15,
        cache_read_input_tokens=2,
        thinking_tokens=3,
    )


def test_map_usage_none() -> None:
    assert not map_usage(None)  # falsy empty Usage