Ctrl K
ComunidadGuías…

app/tests/test_canonical_ir.py

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

Archivo original ↗Preguntar a KiBot
from __future__ import annotations

import pytest

from app.privacy.ir import (
    CanonicalRequest,
    CanonicalRole,
    ErrorEvent,
    FinishEvent,
    GenerationSettings,
    OpaqueExtension,
    StreamEventKind,
    StructuredContent,
    StructuredDataDelta,
    StructuredOutput,
    TextClassification,
    TextContent,
    TextDelta,
    ToolArgumentsDelta,
    ToolCall,
    ToolDefinition,
    ToolResult,
    Turn,
)


def test_canonical_ir_preserves_typed_content_and_tool_structures() -> None:
    request = CanonicalRequest(
        model="model-test",
        turns=(
            Turn(
                role=CanonicalRole.SYSTEM,
                content=(TextContent("Keep answers short", TextClassification.INSTRUCTION),),
            ),
            Turn(
                role=CanonicalRole.USER,
                content=(
                    TextContent("Weather in Paris", TextClassification.USER_TEXT),
                    StructuredContent({"units": "celsius"}),
                ),
            ),
            Turn(
                role=CanonicalRole.ASSISTANT,
                content=(ToolCall("call-1", "weather", {"city": "Paris"}),),
            ),
            Turn(
                role=CanonicalRole.TOOL,
                content=(ToolResult("call-1", "weather", {"temperature": 21}),),
            ),
        ),
        tools=(
            ToolDefinition(
                name="weather",
                description="Read weather",
                input_schema={
                    "type": "object",
                    "properties": {"city": {"type": "string"}},
                    "required": ["city"],
                    "additionalProperties": False,
                },
                strict=True,
            ),
        ),
        structured_output=StructuredOutput(
            name="forecast",
            schema={
                "type": "object",
                "properties": {"temperature": {"type": "number"}},
                "required": ["temperature"],
                "additionalProperties": False,
            },
            strict=True,
        ),
        settings=GenerationSettings(max_output_tokens=256, temperature=0.2),
    )

    assert request.turns[0].content[0].classification is TextClassification.INSTRUCTION
    assert request.turns[2].content[0].arguments == {"city": "Paris"}
    assert request.turns[3].content[0].result == {"temperature": 21}
    assert request.tools[0].input_schema["additionalProperties"] is False
    assert request.structured_output.schema["required"] == ["temperature"]


def test_ir_copies_json_values_and_rejects_non_json_or_invalid_schemas() -> None:
    source = {"nested": ["before"]}
    content = StructuredContent(source)
    source["nested"].append("after")
    assert content.value == {"nested": ["before"]}

    with pytest.raises(ValueError, match="JSON-compatible"):
        StructuredContent({"bad": object()})
    with pytest.raises(ValueError, match="object schema"):
        ToolDefinition("tool", "desc", {"type": "array"})
    with pytest.raises(ValueError, match="object schema"):
        StructuredOutput("result", {"type": "string"})


def test_opaque_extensions_require_an_explicit_trust_marker() -> None:
    with pytest.raises(ValueError, match="explicitly trusted"):
        OpaqueExtension(provider="openai-responses", name="metadata", value={}, trusted=False)

    extension = OpaqueExtension(
        provider="openai-responses",
        name="metadata",
        value={"trace": "synthetic"},
        trusted=True,
    )
    assert extension.trusted is True


def test_stream_event_types_are_explicit_and_path_aware() -> None:
    events = (
        TextDelta(path=("choices", 1, "delta", "content"), text="hello"),
        ToolArgumentsDelta(call_id="call-1", path=("tools", 0), fragment='{"x":'),
        StructuredDataDelta(path=("output", 0), fragment='{"answer":'),
        FinishEvent(reason="stop"),
        ErrorEvent(code="invalid_stream", message="bad event"),
    )

    assert [event.kind for event in events] == [
        StreamEventKind.TEXT_DELTA,
        StreamEventKind.TOOL_ARGUMENTS_DELTA,
        StreamEventKind.STRUCTURED_DATA_DELTA,
        StreamEventKind.FINISH,
        StreamEventKind.ERROR,
    ]