3768 lines
125 KiB
Python
3768 lines
125 KiB
Python
import abc
|
|
import builtins
|
|
import importlib
|
|
import inspect
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from collections.abc import AsyncIterator, Callable, Iterator
|
|
from dataclasses import asdict, dataclass
|
|
from enum import Enum
|
|
from importlib.metadata import version
|
|
from inspect import Parameter, Signature
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Any, Literal, cast
|
|
|
|
import pytest
|
|
from pydantic import BaseModel, Field
|
|
from typing_extensions import Required, TypedDict
|
|
|
|
import integration_tests._contract_support as contract_support
|
|
from integration_tests._contract_support import (
|
|
OptionalDependencyInstallation,
|
|
SubmoduleExportPolicy,
|
|
_callable_contract,
|
|
_default_contract,
|
|
_parameter_contract,
|
|
_public_class_member_contract,
|
|
_validate_parameter_contract,
|
|
_validate_public_class_contract,
|
|
_validate_public_property_contract,
|
|
_validate_public_type_alias_contract,
|
|
_validate_public_typed_dict_contract,
|
|
build_released_api_contract,
|
|
load_api_contract,
|
|
load_submodule_export_policy,
|
|
validate_released_api_contract,
|
|
)
|
|
|
|
CONTRACT = Path(__file__).parent / "fixtures" / "released_api_contract.json"
|
|
|
|
|
|
def _release_policy(
|
|
modules: dict[str, dict[str, dict[str, str]]],
|
|
*,
|
|
dependency_installations: tuple[OptionalDependencyInstallation, ...] = (),
|
|
canonical_imports: tuple[dict[str, str], ...] = (),
|
|
public_class_contracts: tuple[dict[str, Any], ...] = (),
|
|
public_properties: tuple[dict[str, Any], ...] = (),
|
|
public_type_aliases: tuple[dict[str, str], ...] = (),
|
|
public_typed_dicts: tuple[dict[str, Any], ...] = (),
|
|
) -> SubmoduleExportPolicy:
|
|
return SubmoduleExportPolicy(
|
|
modules=modules,
|
|
dependency_installations=dependency_installations,
|
|
canonical_imports=canonical_imports,
|
|
public_class_contracts=public_class_contracts,
|
|
public_properties=public_properties,
|
|
public_type_aliases=public_type_aliases,
|
|
public_typed_dicts=public_typed_dicts,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("released", "changed"),
|
|
[(False, 0), (1, 1.0)],
|
|
)
|
|
def test_literal_default_contract_preserves_exact_builtin_type(
|
|
released: object,
|
|
changed: object,
|
|
) -> None:
|
|
assert _default_contract(released) != _default_contract(changed)
|
|
|
|
def released_callable(value: object = released) -> None:
|
|
_ = value
|
|
|
|
def changed_callable(value: object = changed) -> None:
|
|
_ = value
|
|
|
|
errors = _validate_parameter_contract(
|
|
"Example",
|
|
_parameter_contract(released_callable),
|
|
_parameter_contract(changed_callable),
|
|
)
|
|
|
|
assert len(errors) == 1
|
|
assert "changed its released positional parameter prefix" in errors[0]
|
|
|
|
|
|
def test_type_default_contract_preserves_identity() -> None:
|
|
assert _default_contract(int) == {
|
|
"kind": "type",
|
|
"identity": "builtins.int",
|
|
}
|
|
|
|
def released_callable(value: type = int) -> None:
|
|
_ = value
|
|
|
|
def changed_callable(value: type = float) -> None:
|
|
_ = value
|
|
|
|
errors = _validate_parameter_contract(
|
|
"Example",
|
|
_parameter_contract(released_callable),
|
|
_parameter_contract(changed_callable),
|
|
)
|
|
|
|
assert len(errors) == 1
|
|
assert "changed its released positional parameter prefix" in errors[0]
|
|
|
|
|
|
def test_optional_dependency_for_module_import_uses_canonical_bindings() -> None:
|
|
contract = {
|
|
"required_submodule_exports": {
|
|
"agents.voice": {
|
|
"names": ["VoicePipeline"],
|
|
"optional_bindings": {"VoicePipeline": "numpy"},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [
|
|
{
|
|
"canonical_module": "agents.voice.pipeline",
|
|
"canonical_name": "VoicePipeline",
|
|
"module": "agents.voice",
|
|
"name": "VoicePipeline",
|
|
}
|
|
],
|
|
}
|
|
|
|
assert (
|
|
contract_support._optional_dependency_for_module_import(contract, "agents.voice.pipeline")
|
|
== "numpy"
|
|
)
|
|
assert (
|
|
contract_support._optional_dependency_for_binding(
|
|
contract, "agents.voice.pipeline", "VoicePipeline"
|
|
)
|
|
== "numpy"
|
|
)
|
|
|
|
|
|
def test_released_api_contract_fixture_matches_installed_version() -> None:
|
|
contract = load_api_contract(CONTRACT)
|
|
assert contract["baseline"] == f"v{version('openai-agents')}"
|
|
assert len(contract["baseline_commit"]) == 40
|
|
if contract["baseline"] == "v0.19.4":
|
|
assert contract["baseline_commit"] == "9bfad15ab8297fbb2afe389c983a5cb573eeef56"
|
|
assert all(
|
|
field["name"] != "preserve_raw_usage"
|
|
for field in contract["callables"]["ModelSettings"]["dataclass_fields"]
|
|
)
|
|
assert set(contract["callables"]["Runner"]["members"]) == {
|
|
"run",
|
|
"run_streamed",
|
|
"run_sync",
|
|
}
|
|
assert {"final_output_as", "release_agents", "to_input_list", "to_state"}.issubset(
|
|
contract["callables"]["RunResult"]["members"]
|
|
)
|
|
assert {"from_json", "to_json"}.issubset(contract["callables"]["RunState"]["members"])
|
|
assert contract["public_properties"] == [
|
|
{
|
|
"module": "agents.result",
|
|
"class_name": "RunResultBase",
|
|
"names": ["agent_tool_invocation", "last_agent", "last_response_id"],
|
|
},
|
|
{
|
|
"module": "agents.result",
|
|
"class_name": "RunResult",
|
|
"names": ["agent_tool_invocation", "last_agent", "last_response_id"],
|
|
},
|
|
{
|
|
"module": "agents.result",
|
|
"class_name": "RunResultStreaming",
|
|
"names": [
|
|
"agent_tool_invocation",
|
|
"last_agent",
|
|
"last_response_id",
|
|
"run_loop_exception",
|
|
],
|
|
},
|
|
]
|
|
|
|
|
|
def test_callable_contract_ignores_typing_aliases() -> None:
|
|
alias = Callable[[str], None]
|
|
agents_module = SimpleNamespace(__all__=["Callback"], Callback=alias)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
assert updated["required_top_level_exports"] == ["Callback"]
|
|
assert updated["callables"] == {}
|
|
|
|
|
|
def test_constructor_contract_allows_optional_suffixes_only() -> None:
|
|
def released(value: str) -> None:
|
|
_ = value
|
|
|
|
def compatible(value: str, optional: int = 1, *, named: bool = False) -> None:
|
|
_ = (value, optional, named)
|
|
|
|
def compatible_variadic(value: str, *args: object, **kwargs: object) -> None:
|
|
_ = (value, args, kwargs)
|
|
|
|
def incompatible(value: str, required: int) -> None:
|
|
_ = (value, required)
|
|
|
|
released_contract = _parameter_contract(released)
|
|
|
|
assert (
|
|
_validate_parameter_contract("Example", released_contract, _parameter_contract(compatible))
|
|
== []
|
|
)
|
|
assert (
|
|
_validate_parameter_contract(
|
|
"Example", released_contract, _parameter_contract(compatible_variadic)
|
|
)
|
|
== []
|
|
)
|
|
assert _validate_parameter_contract(
|
|
"Example", released_contract, _parameter_contract(incompatible)
|
|
) == ["Example.required added a required parameter"]
|
|
|
|
def released_variadic(*args: object) -> None:
|
|
_ = args
|
|
|
|
def incompatible_before_variadic(optional: int = 1, *args: object) -> None:
|
|
_ = (optional, args)
|
|
|
|
assert _validate_parameter_contract(
|
|
"VariadicExample",
|
|
_parameter_contract(released_variadic),
|
|
_parameter_contract(incompatible_before_variadic),
|
|
) == [
|
|
"VariadicExample added positional parameters before its released variadic parameter: "
|
|
"[{'name': 'optional', 'kind': 'POSITIONAL_OR_KEYWORD', "
|
|
"'default': {'kind': 'literal', 'type': 'builtins.int', 'value': 1}}]"
|
|
]
|
|
|
|
|
|
def test_public_class_member_contract_tracks_direct_callable_bindings() -> None:
|
|
class Released:
|
|
def instance(self, value: str, optional: int = 1) -> None:
|
|
_ = (value, optional)
|
|
|
|
@classmethod
|
|
def class_method(cls, value: str) -> None:
|
|
_ = (cls, value)
|
|
|
|
@staticmethod
|
|
def static_method(value: str) -> None:
|
|
_ = value
|
|
|
|
@property
|
|
def property_value(self) -> str:
|
|
return "value"
|
|
|
|
assert _public_class_member_contract(Released) == {
|
|
"instance": {
|
|
"binding": "instance",
|
|
"execution_kind": "sync",
|
|
"parameters": [
|
|
{
|
|
"name": "value",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {"kind": "required"},
|
|
},
|
|
{
|
|
"name": "optional",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {
|
|
"kind": "literal",
|
|
"type": "builtins.int",
|
|
"value": 1,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
"class_method": {
|
|
"binding": "class",
|
|
"execution_kind": "sync",
|
|
"parameters": [
|
|
{
|
|
"name": "value",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {"kind": "required"},
|
|
}
|
|
],
|
|
},
|
|
"static_method": {
|
|
"binding": "static",
|
|
"execution_kind": "sync",
|
|
"parameters": [
|
|
{
|
|
"name": "value",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {"kind": "required"},
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def test_curated_public_property_contract_detects_removed_or_changed_properties() -> None:
|
|
class ReleasedBase(metaclass=abc.ABCMeta):
|
|
@abc.abstractmethod
|
|
def base_requirement(self) -> None:
|
|
pass
|
|
|
|
@property
|
|
def retained(self) -> str:
|
|
return "value"
|
|
|
|
class Released(ReleasedBase):
|
|
def base_requirement(self) -> None:
|
|
pass
|
|
|
|
@property
|
|
def retained(self) -> str:
|
|
return "value"
|
|
|
|
@property
|
|
def concrete_only(self) -> str:
|
|
return "value"
|
|
|
|
contract: dict[str, Any] = {
|
|
"public_properties": [
|
|
{
|
|
"module": "agents",
|
|
"class_name": "ReleasedBase",
|
|
"names": ["retained", "removed"],
|
|
},
|
|
{
|
|
"module": "agents",
|
|
"class_name": "Released",
|
|
"names": ["retained", "concrete_only"],
|
|
},
|
|
]
|
|
}
|
|
agents_module = SimpleNamespace(
|
|
__all__=[],
|
|
ReleasedBase=ReleasedBase,
|
|
Released=Released,
|
|
)
|
|
|
|
assert _validate_public_property_contract(contract, agents_module) == [
|
|
"agents.ReleasedBase.removed removed or changed a released public property"
|
|
]
|
|
|
|
class Changed(ReleasedBase):
|
|
def base_requirement(self) -> None:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def new_requirement(self) -> None:
|
|
pass
|
|
|
|
with pytest.raises(TypeError):
|
|
cast(type[Any], Changed)()
|
|
|
|
agents_module.Released = Changed
|
|
|
|
assert _validate_public_property_contract(contract, agents_module) == [
|
|
"agents.ReleasedBase.removed removed or changed a released public property",
|
|
"agents.Released.concrete_only removed or changed a released public property",
|
|
]
|
|
|
|
|
|
def test_curated_public_class_contract_detects_abstract_member_and_state_changes() -> None:
|
|
class ReleasedBase(metaclass=abc.ABCMeta):
|
|
@abc.abstractmethod
|
|
def base_requirement(self) -> None:
|
|
pass
|
|
|
|
class Released(ReleasedBase):
|
|
def base_requirement(self) -> None:
|
|
pass
|
|
|
|
contract: dict[str, Any] = {
|
|
"public_class_contracts": [
|
|
{
|
|
"abstract_members": ["base_requirement"],
|
|
"class_name": "ReleasedBase",
|
|
"module": "agents",
|
|
},
|
|
{
|
|
"abstract": False,
|
|
"class_name": "Released",
|
|
"module": "agents",
|
|
},
|
|
]
|
|
}
|
|
agents_module = SimpleNamespace(__all__=[], ReleasedBase=ReleasedBase, Released=Released)
|
|
|
|
assert _validate_public_class_contract(contract, agents_module) == []
|
|
|
|
class ChangedBase(ReleasedBase):
|
|
@abc.abstractmethod
|
|
def new_requirement(self) -> None:
|
|
pass
|
|
|
|
class Changed(ChangedBase):
|
|
def base_requirement(self) -> None:
|
|
pass
|
|
|
|
def new_requirement(self) -> None:
|
|
pass
|
|
|
|
class ExistingExternalSubclass(ChangedBase):
|
|
def base_requirement(self) -> None:
|
|
pass
|
|
|
|
assert not inspect.isabstract(Changed)
|
|
with pytest.raises(TypeError):
|
|
cast(type[Any], ExistingExternalSubclass)()
|
|
|
|
agents_module.ReleasedBase = ChangedBase
|
|
agents_module.Released = Changed
|
|
|
|
assert _validate_public_class_contract(contract, agents_module) == [
|
|
"agents.ReleasedBase changed its released public abstract members: expected "
|
|
"['base_requirement'], got ['base_requirement', 'new_requirement']"
|
|
]
|
|
|
|
agents_module.Released = ExistingExternalSubclass
|
|
assert _validate_public_class_contract(contract, agents_module) == [
|
|
"agents.ReleasedBase changed its released public abstract members: expected "
|
|
"['base_requirement'], got ['base_requirement', 'new_requirement']",
|
|
"agents.Released changed its released public class state: expected concrete, got abstract",
|
|
]
|
|
|
|
|
|
def test_curated_public_property_contract_supports_factory_return_surfaces(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class ScriptedSession:
|
|
@property
|
|
def calls(self) -> tuple[str, ...]:
|
|
return ()
|
|
|
|
def scripted_session() -> ScriptedSession:
|
|
return ScriptedSession()
|
|
|
|
contract: dict[str, Any] = {
|
|
"public_properties": [
|
|
{
|
|
"module": "agents.testing",
|
|
"factory_name": "scripted_session",
|
|
"names": ["calls", "remaining_steps"],
|
|
}
|
|
]
|
|
}
|
|
testing_module = SimpleNamespace(scripted_session=scripted_session)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda _module_name, _agents_module: testing_module,
|
|
)
|
|
|
|
assert _validate_public_property_contract(contract, testing_module) == [
|
|
"agents.testing.scripted_session.remaining_steps removed or changed a released public "
|
|
"property"
|
|
]
|
|
|
|
|
|
def test_curated_public_type_alias_contract_records_and_validates_members(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class EventA:
|
|
pass
|
|
|
|
class EventB:
|
|
pass
|
|
|
|
EventA.__module__ = "agents.aliases"
|
|
EventA.__qualname__ = "EventA"
|
|
EventB.__module__ = "agents.aliases"
|
|
EventB.__qualname__ = "EventB"
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
aliases_module = SimpleNamespace(
|
|
__all__=[],
|
|
PublicAlias=Literal["b", "a"] | EventB | EventA,
|
|
)
|
|
modules = {
|
|
"agents": agents_module,
|
|
"agents.aliases": aliases_module,
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: modules[module_name],
|
|
)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"public_class_contracts": [],
|
|
"public_properties": [],
|
|
"public_type_aliases": [],
|
|
"public_typed_dicts": [],
|
|
"callables": {},
|
|
}
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{"agents.aliases": {"optional_bindings": {}, "optional_exports": {}}},
|
|
public_type_aliases=(
|
|
{
|
|
"module": "agents.aliases",
|
|
"name": "PublicAlias",
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
assert updated["public_type_aliases"] == [
|
|
{
|
|
"definition": {
|
|
"kind": "union",
|
|
"members": [
|
|
{
|
|
"kind": "literal",
|
|
"values": [
|
|
{
|
|
"kind": "literal",
|
|
"type": "builtins.str",
|
|
"value": "a",
|
|
},
|
|
{
|
|
"kind": "literal",
|
|
"type": "builtins.str",
|
|
"value": "b",
|
|
},
|
|
],
|
|
},
|
|
{"identity": "agents.aliases.EventA", "kind": "type"},
|
|
{"identity": "agents.aliases.EventB", "kind": "type"},
|
|
],
|
|
},
|
|
"module": "agents.aliases",
|
|
"name": "PublicAlias",
|
|
}
|
|
]
|
|
|
|
aliases_module.PublicAlias = Literal["a", "b"] | EventA | EventB
|
|
assert _validate_public_type_alias_contract(updated, agents_module) == []
|
|
|
|
aliases_module.PublicAlias = Literal["a"] | EventA
|
|
errors = _validate_public_type_alias_contract(updated, agents_module)
|
|
assert len(errors) == 1
|
|
assert errors[0].startswith("agents.aliases.PublicAlias changed its released public type alias")
|
|
|
|
aliases_module.PublicAlias = list[str]
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"agents\.aliases\.PublicAlias no longer has a supported released public type alias "
|
|
r"definition: unsupported"
|
|
),
|
|
):
|
|
build_released_api_contract(
|
|
updated,
|
|
baseline="v0.20.1",
|
|
baseline_commit="c" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{"agents.aliases": {"optional_bindings": {}, "optional_exports": {}}},
|
|
public_type_aliases=(
|
|
{
|
|
"module": "agents.aliases",
|
|
"name": "PublicAlias",
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def test_curated_public_typed_dict_contract_detects_field_shape_drift(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class ReleasedState(TypedDict, total=False):
|
|
required_name: Required[str]
|
|
count: int
|
|
|
|
contract: dict[str, Any] = {
|
|
"public_typed_dicts": [
|
|
{
|
|
"module": "agents.testing",
|
|
"class_name": "ReleasedState",
|
|
"fields": [
|
|
{
|
|
"name": "required_name",
|
|
"required": True,
|
|
"annotation": "Required[str]",
|
|
},
|
|
{"name": "count", "required": False, "annotation": "int"},
|
|
],
|
|
}
|
|
]
|
|
}
|
|
testing_module = SimpleNamespace(ReleasedState=ReleasedState)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda _module_name, _agents_module: testing_module,
|
|
)
|
|
|
|
assert _validate_public_typed_dict_contract(contract, testing_module) == []
|
|
|
|
class ChangedState(TypedDict, total=False):
|
|
required_name: int
|
|
|
|
testing_module.ReleasedState = ChangedState
|
|
|
|
assert _validate_public_typed_dict_contract(contract, testing_module) == [
|
|
"agents.testing.ReleasedState.required_name changed its released TypedDict field "
|
|
"contract: expected {'name': 'required_name', 'required': True, 'annotation': "
|
|
"'Required[str]'}, got {'name': 'required_name', 'required': False, "
|
|
"'annotation': 'int'}",
|
|
"agents.testing.ReleasedState.count changed its released TypedDict field contract: "
|
|
"expected {'name': 'count', 'required': False, 'annotation': 'int'}, got None",
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("formatted", "expected"),
|
|
[
|
|
("typing.Required[str]", "Required[str]"),
|
|
("typing_extensions.Required[str]", "Required[str]"),
|
|
("typing.NotRequired[str]", "NotRequired[str]"),
|
|
("typing_extensions.NotRequired[str]", "NotRequired[str]"),
|
|
("mytyping.Required[str]", "mytyping.Required[str]"),
|
|
(
|
|
"vendor.typing_extensions.NotRequired[str]",
|
|
"vendor.typing_extensions.NotRequired[str]",
|
|
),
|
|
("list[typing.Required[str]]", "list[typing.Required[str]]"),
|
|
],
|
|
)
|
|
def test_typed_dict_requiredness_annotation_contract_is_python_version_independent(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
formatted: str,
|
|
expected: str,
|
|
) -> None:
|
|
annotation = object()
|
|
monkeypatch.setattr(
|
|
contract_support.inspect,
|
|
"formatannotation",
|
|
lambda value: formatted if value is annotation else repr(value),
|
|
)
|
|
|
|
assert contract_support._annotation_contract(annotation) == expected
|
|
|
|
|
|
def test_curated_public_property_contract_honors_optional_dependency_availability(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class OptionalClient:
|
|
pass
|
|
|
|
contract: dict[str, Any] = {
|
|
"required_submodule_exports": {
|
|
"agents.optional": {
|
|
"names": ["OptionalClient"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalClient": "optional_backend"},
|
|
}
|
|
},
|
|
"public_properties": [
|
|
{
|
|
"module": "agents.optional",
|
|
"class_name": "OptionalClient",
|
|
"names": ["status"],
|
|
}
|
|
],
|
|
}
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
optional_module = SimpleNamespace(OptionalClient=OptionalClient)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else optional_module
|
|
),
|
|
)
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: False)
|
|
|
|
assert _validate_public_property_contract(contract, agents_module) == []
|
|
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: True)
|
|
|
|
assert _validate_public_property_contract(contract, agents_module) == [
|
|
"agents.optional.OptionalClient.status removed or changed a released public property"
|
|
]
|
|
|
|
|
|
def test_public_class_member_contract_tracks_only_sdk_owned_inherited_methods() -> None:
|
|
class ExternalBase:
|
|
def external_method(self) -> None:
|
|
return None
|
|
|
|
class SDKBase(ExternalBase):
|
|
__module__ = "agents.contract_test"
|
|
|
|
def instance_method(self, value: str) -> None:
|
|
_ = value
|
|
|
|
@classmethod
|
|
def class_method(cls, value: str) -> None:
|
|
_ = (cls, value)
|
|
|
|
@staticmethod
|
|
def shadowed_method(value: str) -> None:
|
|
_ = value
|
|
|
|
def shadowed_method(_self: object) -> str:
|
|
return "value"
|
|
|
|
Released = type(
|
|
"Released",
|
|
(SDKBase,),
|
|
{"shadowed_method": property(shadowed_method)},
|
|
)
|
|
|
|
assert _public_class_member_contract(Released) == {
|
|
"class_method": {
|
|
"binding": "class",
|
|
"execution_kind": "sync",
|
|
"parameters": [
|
|
{
|
|
"name": "value",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {"kind": "required"},
|
|
}
|
|
],
|
|
},
|
|
"instance_method": {
|
|
"binding": "instance",
|
|
"execution_kind": "sync",
|
|
"parameters": [
|
|
{
|
|
"name": "value",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {"kind": "required"},
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def test_callable_contract_preserves_wrapped_function_signature() -> None:
|
|
def released(value: str, optional: int = 1) -> str:
|
|
return value * optional
|
|
|
|
def middle(*args: object, **kwargs: object) -> object:
|
|
_ = (args, kwargs)
|
|
return None
|
|
|
|
def outer(*args: object, **kwargs: object) -> object:
|
|
_ = (args, kwargs)
|
|
return None
|
|
|
|
middle.__wrapped__ = released # type: ignore[attr-defined]
|
|
outer.__wrapped__ = middle # type: ignore[attr-defined]
|
|
|
|
assert _callable_contract(outer)["parameters"] == [
|
|
{
|
|
"name": "value",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {"kind": "required"},
|
|
},
|
|
{
|
|
"name": "optional",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {
|
|
"kind": "literal",
|
|
"type": "builtins.int",
|
|
"value": 1,
|
|
},
|
|
},
|
|
]
|
|
|
|
|
|
def test_released_public_class_member_contract_rejects_breaking_changes() -> None:
|
|
class Released:
|
|
def inherited(self, value: str) -> None:
|
|
_ = value
|
|
|
|
@classmethod
|
|
def changed_binding(cls, value: str) -> None:
|
|
_ = (cls, value)
|
|
|
|
@staticmethod
|
|
def removed(value: str) -> None:
|
|
_ = value
|
|
|
|
def changed_signature(self, value: str, optional: int = 1) -> None:
|
|
_ = (value, optional)
|
|
|
|
class CompatibleBase:
|
|
__module__ = "agents.contract_test"
|
|
|
|
def inherited(self, value: str, optional: int = 1) -> None:
|
|
_ = (value, optional)
|
|
|
|
class Incompatible(CompatibleBase):
|
|
@staticmethod
|
|
def changed_binding(value: str) -> None:
|
|
_ = value
|
|
|
|
def changed_signature(self, renamed: str, optional: int = 1) -> None:
|
|
_ = (renamed, optional)
|
|
|
|
agents_module = SimpleNamespace(__all__=["Released"], Released=Incompatible)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": ["Released"],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {"Released": _callable_contract(Released)},
|
|
}
|
|
|
|
errors = validate_released_api_contract(contract, agents_module=agents_module)
|
|
|
|
assert errors == [
|
|
"Released.changed_binding changed binding from class to static",
|
|
"Released.removed removed a released public method",
|
|
"Released.changed_signature changed its released positional parameter prefix: "
|
|
"expected [{'name': 'value', 'kind': 'POSITIONAL_OR_KEYWORD', "
|
|
"'default': {'kind': 'required'}}, {'name': 'optional', "
|
|
"'kind': 'POSITIONAL_OR_KEYWORD', 'default': {'kind': 'literal', "
|
|
"'type': 'builtins.int', 'value': 1}}], got [{'name': 'renamed', "
|
|
"'kind': 'POSITIONAL_OR_KEYWORD', 'default': {'kind': 'required'}}, "
|
|
"{'name': 'optional', 'kind': 'POSITIONAL_OR_KEYWORD', "
|
|
"'default': {'kind': 'literal', 'type': 'builtins.int', 'value': 1}}]",
|
|
"Released.changed_signature.renamed added a required parameter",
|
|
]
|
|
|
|
|
|
def test_released_callable_contract_rejects_execution_kind_changes() -> None:
|
|
async def released_async(value: str) -> str:
|
|
return value
|
|
|
|
def released_sync(value: str) -> str:
|
|
return value
|
|
|
|
def changed_to_sync(value: str) -> str:
|
|
return value
|
|
|
|
async def changed_to_async(value: str) -> str:
|
|
return value
|
|
|
|
def released_generator(value: str) -> Iterator[str]:
|
|
yield value
|
|
|
|
def changed_generator_to_sync(value: str) -> str:
|
|
return value
|
|
|
|
class ReleasedBase:
|
|
__module__ = "agents.contract_test"
|
|
|
|
@classmethod
|
|
async def inherited_async(cls, value: str) -> str:
|
|
_ = cls
|
|
return value
|
|
|
|
class Released(ReleasedBase):
|
|
async def direct_async(self, value: str) -> str:
|
|
return value
|
|
|
|
@staticmethod
|
|
def direct_sync(value: str) -> str:
|
|
return value
|
|
|
|
async def direct_async_generator(self, value: str) -> AsyncIterator[str]:
|
|
yield value
|
|
|
|
class ChangedBase:
|
|
__module__ = "agents.contract_test"
|
|
|
|
@classmethod
|
|
def inherited_async(cls, value: str) -> str:
|
|
_ = cls
|
|
return value
|
|
|
|
class Changed(ChangedBase):
|
|
def direct_async(self, value: str) -> str:
|
|
return value
|
|
|
|
@staticmethod
|
|
async def direct_sync(value: str) -> str:
|
|
return value
|
|
|
|
async def direct_async_generator(self, value: str) -> str:
|
|
return value
|
|
|
|
agents_module = SimpleNamespace(
|
|
__all__=["released_async", "released_sync", "released_generator", "Released"],
|
|
released_async=changed_to_sync,
|
|
released_sync=changed_to_async,
|
|
released_generator=changed_generator_to_sync,
|
|
Released=Changed,
|
|
)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [
|
|
"released_async",
|
|
"released_sync",
|
|
"released_generator",
|
|
"Released",
|
|
],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {
|
|
"released_async": _callable_contract(released_async),
|
|
"released_sync": _callable_contract(released_sync),
|
|
"released_generator": _callable_contract(released_generator),
|
|
"Released": _callable_contract(Released),
|
|
},
|
|
}
|
|
|
|
errors = validate_released_api_contract(contract, agents_module=agents_module)
|
|
|
|
assert set(errors) == {
|
|
"released_async changed execution from coroutine to sync",
|
|
"released_sync changed execution from sync to coroutine",
|
|
"released_generator changed execution from generator to sync",
|
|
"Released.direct_async changed execution from coroutine to sync",
|
|
"Released.direct_sync changed execution from sync to coroutine",
|
|
"Released.direct_async_generator changed execution from async_generator to coroutine",
|
|
"Released.inherited_async changed execution from coroutine to sync",
|
|
}
|
|
|
|
|
|
def test_released_opaque_sentinel_default_rejects_unrepresentable_replacement() -> None:
|
|
from agents.tool import _UNSET_FAILURE_ERROR_FUNCTION
|
|
|
|
def released(value: object = _UNSET_FAILURE_ERROR_FUNCTION) -> None:
|
|
_ = value
|
|
|
|
def incompatible(value: object = object()) -> None:
|
|
_ = value
|
|
|
|
released_contract = _parameter_contract(released)
|
|
|
|
assert released_contract[0]["default"] == {
|
|
"kind": "sentinel",
|
|
"identity": "agents.tool._UNSET_FAILURE_ERROR_FUNCTION",
|
|
}
|
|
with pytest.raises(TypeError, match="Unsupported public API default value: builtins.object"):
|
|
_parameter_contract(incompatible)
|
|
|
|
|
|
def test_field_info_default_contract_preserves_the_complete_default() -> None:
|
|
assert _default_contract(Field(default=1)) != _default_contract(Field(default=2))
|
|
|
|
|
|
def test_qualified_submodule_callable_contract_detects_signature_change(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
def released(value: str, optional: int = 1) -> None:
|
|
_ = (value, optional)
|
|
|
|
def incompatible(renamed: str, optional: int = 1) -> None:
|
|
_ = (renamed, optional)
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(released=incompatible)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {"agents.submodule.released": _callable_contract(released)},
|
|
}
|
|
|
|
errors = validate_released_api_contract(contract, agents_module=agents_module)
|
|
|
|
assert any("changed its released positional parameter prefix" in error for error in errors)
|
|
|
|
|
|
def test_release_contract_update_freezes_submodule_only_callable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
def helper(value: str = "default") -> None:
|
|
_ = value
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(helper=helper)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"canonical_imports": [
|
|
{
|
|
"module": "agents.submodule",
|
|
"name": "helper",
|
|
"canonical_module": "agents.submodule",
|
|
"canonical_name": "helper",
|
|
}
|
|
],
|
|
"callables": {},
|
|
}
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
assert updated["callables"]["agents.submodule.helper"] == _callable_contract(helper)
|
|
|
|
|
|
def test_enum_constructor_contract_uses_member_lookup_signature() -> None:
|
|
class ReleasedEnum(Enum):
|
|
VALUE = "value"
|
|
|
|
assert _parameter_contract(ReleasedEnum) == [
|
|
{
|
|
"name": "value",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {"kind": "required"},
|
|
}
|
|
]
|
|
|
|
|
|
def test_released_enum_contract_freezes_members_and_values() -> None:
|
|
class ReleasedEnum(Enum):
|
|
OLD = "old"
|
|
|
|
class CompatibleEnum(Enum):
|
|
OLD = "old"
|
|
NEW = "new"
|
|
|
|
class RenamedEnum(Enum):
|
|
RENAMED = "old"
|
|
|
|
class ChangedValueEnum(Enum):
|
|
OLD = "changed"
|
|
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": ["ReleasedEnum"],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {"ReleasedEnum": _callable_contract(ReleasedEnum)},
|
|
}
|
|
|
|
assert (
|
|
validate_released_api_contract(
|
|
contract,
|
|
agents_module=SimpleNamespace(__all__=["ReleasedEnum"], ReleasedEnum=CompatibleEnum),
|
|
)
|
|
== []
|
|
)
|
|
assert validate_released_api_contract(
|
|
contract,
|
|
agents_module=SimpleNamespace(__all__=["ReleasedEnum"], ReleasedEnum=RenamedEnum),
|
|
) == ["ReleasedEnum.OLD removed or renamed a released enum member"]
|
|
assert validate_released_api_contract(
|
|
contract,
|
|
agents_module=SimpleNamespace(__all__=["ReleasedEnum"], ReleasedEnum=ChangedValueEnum),
|
|
) == [
|
|
"ReleasedEnum.OLD changed its released enum value: expected "
|
|
"{'kind': 'literal', 'type': 'builtins.str', 'value': 'old'}, got "
|
|
"{'kind': 'literal', 'type': 'builtins.str', 'value': 'changed'}"
|
|
]
|
|
|
|
|
|
def test_public_api_contract_requires_real_export_bindings() -> None:
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": ["AgentsException"],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
agents_module = SimpleNamespace(__all__=["AgentsException"])
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Missing released top-level bindings: ['AgentsException']"
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("failure", ["membership", "binding"])
|
|
def test_public_api_contract_requires_released_submodule_exports(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
failure: str,
|
|
) -> None:
|
|
sandbox_error = type("SandboxError", (Exception,), {})
|
|
submodule = SimpleNamespace(
|
|
__all__=[] if failure == "membership" else ["SandboxError"],
|
|
SandboxError=sandbox_error,
|
|
)
|
|
if failure == "binding":
|
|
del submodule.SandboxError
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["SandboxError"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
errors = validate_released_api_contract(contract, agents_module=agents_module)
|
|
|
|
expected_kind = "exports" if failure == "membership" else "bindings"
|
|
assert errors == [f"Missing released agents.submodule {expected_kind}: ['SandboxError']"]
|
|
|
|
|
|
def test_public_api_contract_rejects_missing_self_canonical_binding() -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [
|
|
{
|
|
"module": "agents",
|
|
"name": "Missing",
|
|
"canonical_module": "agents",
|
|
"canonical_name": "Missing",
|
|
}
|
|
],
|
|
"callables": {},
|
|
}
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"agents.Missing no longer resolves to agents.Missing"
|
|
]
|
|
|
|
|
|
def test_public_api_contract_allows_declared_platform_import_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.platform_specific"],
|
|
"platform_import_errors": [
|
|
{
|
|
"module": "agents.platform_specific",
|
|
"platforms": ["win32"],
|
|
"error_type": "ImportError",
|
|
"message_contains": "not supported on Windows",
|
|
}
|
|
],
|
|
"canonical_imports": [
|
|
{
|
|
"module": "agents.platform_specific",
|
|
"name": "PlatformBinding",
|
|
"canonical_module": "agents.platform_specific",
|
|
"canonical_name": "PlatformBinding",
|
|
}
|
|
],
|
|
"callables": {},
|
|
}
|
|
|
|
def raise_platform_error(module_name: str, _: Any) -> Any:
|
|
assert module_name == "agents.platform_specific"
|
|
raise ImportError("Backend is not supported on Windows. Use another backend.")
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_import_contract_module", raise_platform_error)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == []
|
|
|
|
|
|
def test_public_api_contract_allows_binding_with_unavailable_canonical_module(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
parent_module = SimpleNamespace()
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.platform_parent", "agents.platform_child"],
|
|
"platform_import_errors": [
|
|
{
|
|
"module": "agents.platform_child",
|
|
"platforms": ["win32"],
|
|
"error_type": "ImportError",
|
|
"message_contains": "not supported on Windows",
|
|
}
|
|
],
|
|
"canonical_imports": [
|
|
{
|
|
"module": "agents.platform_parent",
|
|
"name": "PlatformBinding",
|
|
"canonical_module": "agents.platform_child",
|
|
"canonical_name": "PlatformBinding",
|
|
}
|
|
],
|
|
"callables": {},
|
|
}
|
|
|
|
def import_platform_module(module_name: str, _: Any) -> Any:
|
|
if module_name == "agents.platform_parent":
|
|
return parent_module
|
|
assert module_name == "agents.platform_child"
|
|
raise ImportError("Backend is not supported on Windows. Use another backend.")
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_import_contract_module", import_platform_module)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == []
|
|
|
|
|
|
def test_public_api_contract_rejects_unexpected_platform_import_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.platform_specific"],
|
|
"platform_import_errors": [
|
|
{
|
|
"module": "agents.platform_specific",
|
|
"platforms": ["win32"],
|
|
"error_type": "ImportError",
|
|
"message_contains": "not supported on Windows",
|
|
}
|
|
],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
def raise_unexpected_error(module_name: str, _: Any) -> Any:
|
|
assert module_name == "agents.platform_specific"
|
|
raise ImportError("Unexpected dependency failure")
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_import_contract_module", raise_unexpected_error)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Failed to import released module agents.platform_specific: "
|
|
"ImportError('Unexpected dependency failure')"
|
|
]
|
|
|
|
|
|
def test_public_api_contract_rejects_same_named_foreign_platform_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.platform_specific"],
|
|
"platform_import_errors": [
|
|
{
|
|
"module": "agents.platform_specific",
|
|
"platforms": ["win32"],
|
|
"error_type": "ImportError",
|
|
"message_contains": "not supported on Windows",
|
|
}
|
|
],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
foreign_import_error = type("ImportError", (Exception,), {})
|
|
|
|
def raise_foreign_error(module_name: str, _: Any) -> Any:
|
|
assert module_name == "agents.platform_specific"
|
|
raise foreign_import_error("Backend is not supported on Windows.")
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_import_contract_module", raise_foreign_error)
|
|
|
|
errors = validate_released_api_contract(contract, agents_module=agents_module)
|
|
assert len(errors) == 1
|
|
assert errors[0].startswith("Failed to import released module agents.platform_specific:")
|
|
|
|
|
|
def test_public_api_contract_rejects_required_dataclass_suffix() -> None:
|
|
@dataclass
|
|
class Incompatible:
|
|
value: str
|
|
required_suffix: int
|
|
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {
|
|
"ContractExample": {
|
|
"kind": "class",
|
|
"parameters": [
|
|
{
|
|
"name": "value",
|
|
"kind": "POSITIONAL_OR_KEYWORD",
|
|
"default": {"kind": "required"},
|
|
}
|
|
],
|
|
"dataclass_fields": [
|
|
{"name": "value", "init": True, "default": {"kind": "required"}}
|
|
],
|
|
}
|
|
},
|
|
}
|
|
agents_module = SimpleNamespace(__all__=[], ContractExample=Incompatible)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"ContractExample.required_suffix added a required parameter",
|
|
"ContractExample.required_suffix added a required dataclass field",
|
|
]
|
|
|
|
|
|
def test_callable_contract_tracks_pydantic_model_fields() -> None:
|
|
class Model(BaseModel):
|
|
required: str
|
|
optional: int = 1
|
|
generated: list[str] = Field(default_factory=list)
|
|
|
|
Model.__signature__ = Signature([Parameter("data", kind=Parameter.VAR_KEYWORD)])
|
|
callable_contract = _callable_contract(Model)
|
|
|
|
assert callable_contract["parameters"] == [
|
|
{"name": "data", "kind": "VAR_KEYWORD", "default": {"kind": "required"}}
|
|
]
|
|
assert callable_contract["model_fields"] == [
|
|
{"name": "required", "default": {"kind": "required"}},
|
|
{
|
|
"name": "optional",
|
|
"default": {"kind": "literal", "type": "builtins.int", "value": 1},
|
|
},
|
|
{
|
|
"name": "generated",
|
|
"default": {"kind": "factory", "factory": "builtins.list"},
|
|
},
|
|
]
|
|
|
|
|
|
def test_public_api_contract_validates_pydantic_model_fields() -> None:
|
|
class Released(BaseModel):
|
|
required: str
|
|
optional: int = 1
|
|
|
|
class Compatible(BaseModel):
|
|
optional: int = 1
|
|
required: str
|
|
added_optional: bool = False
|
|
|
|
class Renamed(BaseModel):
|
|
renamed: str
|
|
optional: int = 1
|
|
|
|
class ChangedDefault(BaseModel):
|
|
required: str
|
|
optional: int = 2
|
|
|
|
class AddedRequired(BaseModel):
|
|
required: str
|
|
optional: int = 1
|
|
added_required: bool
|
|
|
|
opaque_signature = Signature([Parameter("data", kind=Parameter.VAR_KEYWORD)])
|
|
for model in (Released, Compatible, Renamed, ChangedDefault, AddedRequired):
|
|
model.__signature__ = opaque_signature
|
|
|
|
released_callable = _callable_contract(Released)
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {"Model": released_callable},
|
|
}
|
|
|
|
def validate(model: type[BaseModel]) -> list[str]:
|
|
return validate_released_api_contract(
|
|
contract,
|
|
agents_module=SimpleNamespace(__all__=[], Model=model),
|
|
)
|
|
|
|
assert validate(Compatible) == []
|
|
assert validate(Renamed) == [
|
|
"Model.required changed its released Pydantic model field contract: "
|
|
"expected {'name': 'required', 'default': {'kind': 'required'}}, got None",
|
|
"Model.renamed added a required Pydantic model field",
|
|
]
|
|
assert validate(ChangedDefault) == [
|
|
"Model.optional changed its released Pydantic model field contract: "
|
|
"expected {'name': 'optional', 'default': {'kind': 'literal', "
|
|
"'type': 'builtins.int', 'value': 1}}, got {'name': 'optional', "
|
|
"'default': {'kind': 'literal', 'type': 'builtins.int', 'value': 2}}"
|
|
]
|
|
assert validate(AddedRequired) == ["Model.added_required added a required Pydantic model field"]
|
|
|
|
legacy_contract = {
|
|
**contract,
|
|
"callables": {
|
|
"Model": {
|
|
key: value for key, value in released_callable.items() if key != "model_fields"
|
|
}
|
|
},
|
|
}
|
|
assert (
|
|
validate_released_api_contract(
|
|
legacy_contract,
|
|
agents_module=SimpleNamespace(__all__=[], Model=Renamed),
|
|
)
|
|
== []
|
|
)
|
|
|
|
|
|
def test_release_contract_update_freezes_new_exports_and_callables() -> None:
|
|
@dataclass
|
|
class Existing:
|
|
value: str
|
|
optional: int = 1
|
|
|
|
@dataclass
|
|
class NewPublic:
|
|
name: str
|
|
enabled: bool = True
|
|
|
|
def new_helper() -> None:
|
|
return None
|
|
|
|
class Uninspectable:
|
|
__signature__ = "invalid"
|
|
|
|
class NewEnum(Enum):
|
|
VALUE = "value"
|
|
|
|
agents_module = SimpleNamespace(
|
|
__all__=["new_helper", "Existing", "NewPublic", "NewEnum", "Uninspectable"],
|
|
Existing=Existing,
|
|
new_helper=new_helper,
|
|
NewPublic=NewPublic,
|
|
NewEnum=NewEnum,
|
|
Uninspectable=Uninspectable,
|
|
)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": ["Existing"],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
assert updated["baseline"] == "v0.20.0"
|
|
assert updated["baseline_commit"] == "b" * 40
|
|
assert updated["required_top_level_exports"] == [
|
|
"Existing",
|
|
"new_helper",
|
|
"NewPublic",
|
|
"NewEnum",
|
|
"Uninspectable",
|
|
]
|
|
assert set(updated["callables"]) == {"Existing", "NewEnum", "NewPublic", "new_helper"}
|
|
assert updated["callables"]["Existing"]["kind"] == "class"
|
|
assert updated["callables"]["new_helper"]["kind"] == "function"
|
|
assert [field["name"] for field in updated["callables"]["Existing"]["dataclass_fields"]] == [
|
|
"value",
|
|
"optional",
|
|
]
|
|
assert [field["name"] for field in updated["callables"]["NewPublic"]["dataclass_fields"]] == [
|
|
"name",
|
|
"enabled",
|
|
]
|
|
assert updated["callables"]["NewEnum"]["enum_members"] == [
|
|
{
|
|
"name": "VALUE",
|
|
"value": {"kind": "literal", "type": "builtins.str", "value": "value"},
|
|
}
|
|
]
|
|
assert updated["public_modules"] == ["agents"]
|
|
assert updated["canonical_imports"] == []
|
|
assert updated["required_submodule_exports"] == {}
|
|
|
|
unchanged = build_released_api_contract(
|
|
updated,
|
|
baseline="v0.20.0",
|
|
baseline_commit="c" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
assert unchanged["baseline_commit"] == "b" * 40
|
|
|
|
|
|
def test_release_contract_update_promotes_selected_submodule_exports(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
existing = object()
|
|
added = object()
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["Existing", "Added"], Existing=existing, Added=added)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["Existing"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
assert updated["required_submodule_exports"] == {
|
|
"agents.submodule": {
|
|
"names": ["Existing", "Added"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
}
|
|
|
|
|
|
def test_release_contract_update_freezes_new_sdk_submodule_callable_without_canonical_import(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class Existing:
|
|
pass
|
|
|
|
class NewPublic:
|
|
def __init__(self, value: str, optional: int = 1) -> None:
|
|
self.value = value
|
|
self.optional = optional
|
|
|
|
Existing.__module__ = "agents.submodule"
|
|
NewPublic.__module__ = "agents.submodule"
|
|
agents_module = SimpleNamespace(__all__=["NewPublic"], NewPublic=NewPublic)
|
|
submodule = SimpleNamespace(
|
|
__all__=["Existing", "NewPublic"], Existing=Existing, NewPublic=NewPublic
|
|
)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["Existing"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
assert "agents.submodule.Existing" not in updated["callables"]
|
|
assert updated["callables"]["NewPublic"] == _callable_contract(NewPublic)
|
|
assert updated["callables"]["agents.submodule.NewPublic"] == _callable_contract(NewPublic)
|
|
assert updated["canonical_imports"] == []
|
|
|
|
unchanged = build_released_api_contract(
|
|
updated,
|
|
baseline="v0.20.0",
|
|
baseline_commit="c" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
assert unchanged["callables"]["agents.submodule.NewPublic"] == _callable_contract(NewPublic)
|
|
|
|
class ChangedPublic:
|
|
def __init__(self, value: str, optional: int = 1, *, required: int) -> None:
|
|
self.value = value
|
|
self.optional = optional
|
|
self.required = required
|
|
|
|
ChangedPublic.__module__ = "agents.submodule"
|
|
submodule.NewPublic = ChangedPublic
|
|
|
|
assert validate_released_api_contract(updated, agents_module=agents_module) == [
|
|
"agents.submodule.NewPublic.required added a required parameter"
|
|
]
|
|
|
|
|
|
def test_release_contract_update_skips_new_third_party_submodule_callable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class ExternalPublic:
|
|
pass
|
|
|
|
ExternalPublic.__module__ = "external_package"
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["ExternalPublic"], ExternalPublic=ExternalPublic)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": [],
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
assert "agents.submodule.ExternalPublic" not in updated["callables"]
|
|
|
|
|
|
def test_release_contract_update_preserves_tracked_submodule_callable_on_unsupported_platform(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class OptionalPublic:
|
|
def __init__(self, value: str) -> None:
|
|
self.value = value
|
|
|
|
OptionalPublic.__module__ = "agents.optional_parent"
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
callable_contract = _callable_contract(OptionalPublic)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.optional_parent"],
|
|
"required_submodule_exports": {
|
|
"agents.optional_parent": {
|
|
"names": ["OptionalPublic"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalPublic": "optional_backend"},
|
|
}
|
|
},
|
|
"optional_dependency_unsupported_platforms": {"optional_backend": ["win32"]},
|
|
"canonical_imports": [],
|
|
"callables": {"agents.optional_parent.OptionalPublic": callable_contract},
|
|
}
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: False)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{
|
|
"agents.optional_parent": {
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalPublic": "optional_backend"},
|
|
}
|
|
},
|
|
dependency_installations=(
|
|
OptionalDependencyInstallation(
|
|
dependency_module="optional_backend",
|
|
extra="optional-provider",
|
|
unsupported_platforms=("win32",),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
assert updated["callables"]["agents.optional_parent.OptionalPublic"] == callable_contract
|
|
|
|
|
|
def test_release_contract_update_preserves_tracked_submodule_callable_on_platform_import_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class PlatformPublic:
|
|
def __init__(self, value: str) -> None:
|
|
self.value = value
|
|
|
|
PlatformPublic.__module__ = "agents.platform_specific"
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
callable_contract = _callable_contract(PlatformPublic)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.platform_specific"],
|
|
"required_submodule_exports": {
|
|
"agents.platform_specific": {
|
|
"names": ["PlatformPublic"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"platform_import_errors": [
|
|
{
|
|
"module": "agents.platform_specific",
|
|
"platforms": ["win32"],
|
|
"error_type": "ImportError",
|
|
"message_contains": "not supported on Windows",
|
|
}
|
|
],
|
|
"canonical_imports": [],
|
|
"callables": {"agents.platform_specific.PlatformPublic": callable_contract},
|
|
}
|
|
|
|
def import_platform_module(module_name: str, _: Any) -> Any:
|
|
assert module_name == "agents.platform_specific"
|
|
raise ImportError("Backend is not supported on Windows.")
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_import_contract_module", import_platform_module)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
assert updated["callables"]["agents.platform_specific.PlatformPublic"] == callable_contract
|
|
|
|
|
|
def test_release_contract_policy_preserves_new_optional_export_in_core_install(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
existing = object()
|
|
optional = object()
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
full_submodule = SimpleNamespace(
|
|
__all__=["Existing", "OptionalBackend"],
|
|
Existing=existing,
|
|
OptionalBackend=optional,
|
|
)
|
|
core_submodule = SimpleNamespace(__all__=["Existing"], Existing=existing)
|
|
imported_submodule = full_submodule
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["Existing"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
def import_module(module_name: str, _agents_module: object) -> object:
|
|
return agents_module if module_name == "agents" else imported_submodule
|
|
|
|
monkeypatch.setattr(contract_support, "_import_contract_module", import_module)
|
|
dependency_available = True
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_optional_dependency_is_available",
|
|
lambda _module_name: dependency_available,
|
|
)
|
|
policy = {
|
|
"agents.submodule": {
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "missing_optional_backend_dependency"},
|
|
}
|
|
}
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(policy),
|
|
)
|
|
dependency_available = False
|
|
imported_submodule = core_submodule
|
|
|
|
assert updated["required_submodule_exports"]["agents.submodule"] == {
|
|
"names": ["Existing", "OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "missing_optional_backend_dependency"},
|
|
}
|
|
assert validate_released_api_contract(updated, agents_module=agents_module) == []
|
|
|
|
|
|
def test_release_contract_policy_promotes_curated_public_state_surfaces(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class NewPublic:
|
|
def __init__(self, value: str, optional: int = 1) -> None:
|
|
self.value = value
|
|
self.optional = optional
|
|
|
|
@property
|
|
def status(self) -> str:
|
|
return "ready"
|
|
|
|
class FactoryResult:
|
|
@property
|
|
def calls(self) -> tuple[str, ...]:
|
|
return ()
|
|
|
|
def create_result() -> FactoryResult:
|
|
return FactoryResult()
|
|
|
|
class PublicState(TypedDict, total=False):
|
|
label: Required[str]
|
|
count: int
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(
|
|
__all__=["NewPublic", "PublicState", "create_result"],
|
|
NewPublic=NewPublic,
|
|
PublicState=PublicState,
|
|
create_result=create_result,
|
|
)
|
|
modules = {
|
|
"agents": agents_module,
|
|
"agents.submodule": submodule,
|
|
"agents.submodule.impl": SimpleNamespace(NewPublic=NewPublic),
|
|
}
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"public_properties": [],
|
|
"public_typed_dicts": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: modules[module_name],
|
|
)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{"agents.submodule": {"optional_bindings": {}, "optional_exports": {}}},
|
|
canonical_imports=(
|
|
{
|
|
"canonical_module": "agents.submodule.impl",
|
|
"canonical_name": "NewPublic",
|
|
"module": "agents.submodule",
|
|
"name": "NewPublic",
|
|
},
|
|
),
|
|
public_class_contracts=(
|
|
{
|
|
"abstract": False,
|
|
"class_name": "NewPublic",
|
|
"module": "agents.submodule",
|
|
},
|
|
),
|
|
public_properties=(
|
|
{
|
|
"class_name": "NewPublic",
|
|
"module": "agents.submodule",
|
|
"names": ["status"],
|
|
},
|
|
{
|
|
"factory_name": "create_result",
|
|
"module": "agents.submodule",
|
|
"names": ["calls"],
|
|
},
|
|
),
|
|
public_typed_dicts=(
|
|
{
|
|
"class_name": "PublicState",
|
|
"module": "agents.submodule",
|
|
"names": ["label", "count"],
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
assert updated["canonical_imports"] == [
|
|
{
|
|
"canonical_module": "agents.submodule.impl",
|
|
"canonical_name": "NewPublic",
|
|
"module": "agents.submodule",
|
|
"name": "NewPublic",
|
|
}
|
|
]
|
|
assert updated["public_properties"] == [
|
|
{
|
|
"class_name": "NewPublic",
|
|
"module": "agents.submodule",
|
|
"names": ["status"],
|
|
},
|
|
{
|
|
"factory_name": "create_result",
|
|
"module": "agents.submodule",
|
|
"names": ["calls"],
|
|
},
|
|
]
|
|
assert updated["public_class_contracts"] == [
|
|
{
|
|
"abstract": False,
|
|
"class_name": "NewPublic",
|
|
"module": "agents.submodule",
|
|
}
|
|
]
|
|
assert updated["public_typed_dicts"] == [
|
|
{
|
|
"class_name": "PublicState",
|
|
"fields": [
|
|
{
|
|
"name": "label",
|
|
"required": True,
|
|
"annotation": "Required[str]",
|
|
},
|
|
{"name": "count", "required": False, "annotation": "int"},
|
|
],
|
|
"module": "agents.submodule",
|
|
}
|
|
]
|
|
assert updated["callables"]["agents.submodule.NewPublic"] == _callable_contract(NewPublic)
|
|
|
|
|
|
def test_typed_dict_only_promotion_updates_baseline_commit(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class PublicState(TypedDict, total=False):
|
|
value: str
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(PublicState=PublicState)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.20.0",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents", "agents.submodule"],
|
|
"required_submodule_exports": {},
|
|
"canonical_imports": [],
|
|
"public_properties": [],
|
|
"optional_dependency_unsupported_platforms": {},
|
|
"submodule_export_exclusions": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: {
|
|
"agents": agents_module,
|
|
"agents.submodule": submodule,
|
|
}[module_name],
|
|
)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{},
|
|
public_typed_dicts=(
|
|
{
|
|
"class_name": "PublicState",
|
|
"module": "agents.submodule",
|
|
"names": ["value"],
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
assert updated["baseline_commit"] == "b" * 40
|
|
|
|
|
|
def test_release_contract_policy_honors_unsupported_platform_during_promotion(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class PlatformBinding:
|
|
def __init__(self, value: str) -> None:
|
|
self.value = value
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
platform_parent = SimpleNamespace(__all__=[])
|
|
optional_parent = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": [
|
|
"agents",
|
|
"agents.platform_parent",
|
|
"agents.platform_child",
|
|
],
|
|
"platform_import_errors": [
|
|
{
|
|
"module": "agents.platform_child",
|
|
"platforms": ["win32"],
|
|
"error_type": "ImportError",
|
|
"message_contains": "not supported on Windows",
|
|
}
|
|
],
|
|
"canonical_imports": [
|
|
{
|
|
"canonical_module": "agents.platform_child",
|
|
"canonical_name": "PlatformBinding",
|
|
"module": "agents.platform_parent",
|
|
"name": "PlatformBinding",
|
|
}
|
|
],
|
|
"callables": {
|
|
"agents.platform_parent.PlatformBinding": _callable_contract(PlatformBinding)
|
|
},
|
|
}
|
|
|
|
def import_module(module_name: str, _agents_module: object) -> object:
|
|
if module_name == "agents":
|
|
return agents_module
|
|
if module_name == "agents.platform_parent":
|
|
return platform_parent
|
|
if module_name == "agents.platform_child":
|
|
raise ImportError("Platform binding is not supported on Windows")
|
|
if module_name == "agents.optional_parent":
|
|
return optional_parent
|
|
raise AssertionError(f"Unexpected import: {module_name}")
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: False)
|
|
monkeypatch.setattr(contract_support, "_import_contract_module", import_module)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{
|
|
"agents.optional_parent": {
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalProvider": "optional_backend"},
|
|
}
|
|
},
|
|
dependency_installations=(
|
|
OptionalDependencyInstallation(
|
|
dependency_module="optional_backend",
|
|
extra="optional-provider",
|
|
unsupported_platforms=("win32",),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
assert updated["optional_dependency_unsupported_platforms"] == {"optional_backend": ["win32"]}
|
|
assert updated["canonical_imports"] == contract["canonical_imports"]
|
|
assert updated["required_submodule_exports"]["agents.optional_parent"] == {
|
|
"names": ["OptionalProvider"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalProvider": "optional_backend"},
|
|
}
|
|
assert updated["callables"]["agents.platform_parent.PlatformBinding"] == _callable_contract(
|
|
PlatformBinding
|
|
)
|
|
assert "agents.optional_parent.OptionalProvider" not in updated["callables"]
|
|
|
|
|
|
def test_release_contract_policy_rejects_new_callable_on_unsupported_platform(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: False)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Cannot promote new canonical callable agents\.optional_parent\.OptionalProvider "
|
|
r"because optional dependency 'optional_backend' is unsupported on 'win32'.*"
|
|
r"release preparation host"
|
|
),
|
|
):
|
|
build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{
|
|
"agents.optional_parent": {
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalProvider": "optional_backend"},
|
|
}
|
|
},
|
|
dependency_installations=(
|
|
OptionalDependencyInstallation(
|
|
dependency_module="optional_backend",
|
|
extra="optional-provider",
|
|
unsupported_platforms=("win32",),
|
|
),
|
|
),
|
|
canonical_imports=(
|
|
{
|
|
"canonical_module": "agents.optional_impl",
|
|
"canonical_name": "OptionalProvider",
|
|
"module": "agents.optional_parent",
|
|
"name": "OptionalProvider",
|
|
},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def test_release_contract_policy_promotes_new_uninspectable_canonical_surface(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class UninspectableMeta(type):
|
|
@property
|
|
def __signature__(cls) -> Signature:
|
|
raise ValueError("signature unavailable")
|
|
|
|
class Uninspectable(metaclass=UninspectableMeta):
|
|
pass
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["Uninspectable"], Uninspectable=Uninspectable)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
modules = {
|
|
"agents": agents_module,
|
|
"agents.submodule": submodule,
|
|
"agents.submodule.impl": submodule,
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: modules[module_name],
|
|
)
|
|
|
|
canonical_entry = {
|
|
"canonical_module": "agents.submodule.impl",
|
|
"canonical_name": "Uninspectable",
|
|
"module": "agents.submodule",
|
|
"name": "Uninspectable",
|
|
}
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{
|
|
"agents.submodule": {
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
canonical_imports=(canonical_entry,),
|
|
),
|
|
)
|
|
|
|
assert updated["canonical_imports"] == [canonical_entry]
|
|
assert "agents.submodule.Uninspectable" not in updated["callables"]
|
|
|
|
|
|
def test_release_contract_policy_keeps_existing_uninspectable_canonical_surface(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class UninspectableMeta(type):
|
|
@property
|
|
def __signature__(cls) -> Signature:
|
|
raise ValueError("signature unavailable")
|
|
|
|
class Uninspectable(metaclass=UninspectableMeta):
|
|
pass
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["Uninspectable"], Uninspectable=Uninspectable)
|
|
canonical_entry = {
|
|
"canonical_module": "agents.submodule.impl",
|
|
"canonical_name": "Uninspectable",
|
|
"module": "agents.submodule",
|
|
"name": "Uninspectable",
|
|
}
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents", "agents.submodule"],
|
|
"canonical_imports": [canonical_entry],
|
|
"callables": {},
|
|
}
|
|
modules = {
|
|
"agents": agents_module,
|
|
"agents.submodule": submodule,
|
|
"agents.submodule.impl": submodule,
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: modules[module_name],
|
|
)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
assert updated["canonical_imports"] == [canonical_entry]
|
|
assert "agents.submodule.Uninspectable" not in updated["callables"]
|
|
|
|
|
|
def test_public_api_contract_skips_optional_surface_on_frozen_unsupported_platform(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
class OptionalBackend:
|
|
def __init__(self, value: str) -> None:
|
|
self.value = value
|
|
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "optional_backend"},
|
|
}
|
|
},
|
|
"optional_dependency_unsupported_platforms": {"optional_backend": ["win32"]},
|
|
"canonical_imports": [
|
|
{
|
|
"canonical_module": "agents.submodule.impl",
|
|
"canonical_name": "OptionalBackend",
|
|
"module": "agents.submodule",
|
|
"name": "OptionalBackend",
|
|
}
|
|
],
|
|
"callables": {"agents.submodule.OptionalBackend": _callable_contract(OptionalBackend)},
|
|
}
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: True)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == []
|
|
|
|
|
|
def test_public_api_contract_allows_present_optional_surface_on_unsupported_platform(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
optional_export = object()
|
|
optional_binding = object()
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(
|
|
__all__=["OptionalExport", "OptionalBinding"],
|
|
OptionalExport=optional_export,
|
|
OptionalBinding=optional_binding,
|
|
)
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalExport", "OptionalBinding"],
|
|
"optional_bindings": {"OptionalBinding": "optional_binding_dependency"},
|
|
"optional_exports": {"OptionalExport": "optional_export_dependency"},
|
|
}
|
|
},
|
|
"optional_dependency_unsupported_platforms": {
|
|
"optional_binding_dependency": ["win32"],
|
|
"optional_export_dependency": ["win32"],
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: True)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == []
|
|
|
|
|
|
def test_public_api_contract_rejects_dangling_optional_export_on_unsupported_platform(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["OptionalExport"])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalExport"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalExport": "optional_export_dependency"},
|
|
}
|
|
},
|
|
"optional_dependency_unsupported_platforms": {"optional_export_dependency": ["win32"]},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: True)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Invalid released agents.submodule optional dependency declaration: "
|
|
"'OptionalExport' remains in __all__ on an unsupported platform but its "
|
|
"binding is unavailable"
|
|
]
|
|
|
|
|
|
def test_public_api_contract_rejects_dangling_optional_binding_on_unsupported_platform(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["OptionalBinding"])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBinding"],
|
|
"optional_bindings": {"OptionalBinding": "optional_binding_dependency"},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"optional_dependency_unsupported_platforms": {"optional_binding_dependency": ["win32"]},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(sys, "platform", "win32")
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: True)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Invalid released agents.submodule optional dependency declaration: "
|
|
"'OptionalBinding' remains in __all__ on an unsupported platform but its "
|
|
"binding is unavailable"
|
|
]
|
|
|
|
|
|
def test_public_api_contract_requires_optional_surface_on_supported_platform(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "optional_backend"},
|
|
}
|
|
},
|
|
"optional_dependency_unsupported_platforms": {"optional_backend": ["win32"]},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
monkeypatch.setattr(contract_support, "_optional_dependency_is_available", lambda _name: True)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Missing released agents.submodule exports: ['OptionalBackend']",
|
|
"Missing released agents.submodule bindings: ['OptionalBackend']",
|
|
]
|
|
|
|
|
|
def test_release_contract_policy_rejects_unavailable_dependency_module(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_optional_dependency_is_available",
|
|
lambda _module_name: False,
|
|
)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="submodule export policy dependency modules are unavailable: "
|
|
r"\['mistyped_dependency'\]",
|
|
):
|
|
build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{
|
|
"agents.submodule": {
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "mistyped_dependency"},
|
|
}
|
|
}
|
|
),
|
|
)
|
|
|
|
|
|
def test_release_contract_policy_adds_new_public_optional_module(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
optional = object()
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["OptionalBackend"], OptionalBackend=optional)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_optional_dependency_is_available",
|
|
lambda _module_name: True,
|
|
)
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
updated = build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{
|
|
"agents.new_submodule": {
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "optional_backend"},
|
|
}
|
|
}
|
|
),
|
|
)
|
|
|
|
assert updated["public_modules"] == ["agents", "agents.new_submodule"]
|
|
assert updated["required_submodule_exports"]["agents.new_submodule"] == {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "optional_backend"},
|
|
}
|
|
|
|
|
|
def test_release_contract_policy_rejects_unimportable_new_public_module() -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=r"Cannot import submodule export policy module agents\.typo: ModuleNotFoundError",
|
|
):
|
|
build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{"agents.typo": {"optional_bindings": {}, "optional_exports": {}}}
|
|
),
|
|
)
|
|
|
|
|
|
def test_release_contract_policy_rejects_new_module_outside_agents_package() -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="new submodule export policy modules must be under the agents package: "
|
|
r"\['external_package'\]",
|
|
):
|
|
build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
release_policy=_release_policy(
|
|
{"external_package": {"optional_bindings": {}, "optional_exports": {}}}
|
|
),
|
|
)
|
|
|
|
|
|
def test_load_submodule_export_policy_rejects_unknown_fields(tmp_path: Path) -> None:
|
|
policy_path = tmp_path / "policy.json"
|
|
policy_path.write_text(
|
|
'{"modules": {"agents.submodule": {"optional_export": {}}}, "optional_dependencies": {}}',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match=r"submodule export policy for agents.submodule has unknown fields: "
|
|
r"\['optional_export'\]",
|
|
):
|
|
load_submodule_export_policy(policy_path)
|
|
|
|
|
|
def test_load_submodule_export_policy_requires_dependency_installations(tmp_path: Path) -> None:
|
|
policy_path = tmp_path / "policy.json"
|
|
policy_path.write_text(
|
|
'{"modules": {"agents.submodule": {"optional_exports": '
|
|
'{"OptionalBackend": "optional_backend"}}}, "optional_dependencies": {}}',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="submodule export policy dependencies are missing installation declarations: "
|
|
r"\['optional_backend'\]",
|
|
):
|
|
load_submodule_export_policy(policy_path)
|
|
|
|
|
|
def test_load_submodule_export_policy_collects_artifact_installations(tmp_path: Path) -> None:
|
|
policy_path = tmp_path / "policy.json"
|
|
policy_path.write_text(
|
|
'{"canonical_imports": [{"canonical_module": "agents.submodule.impl", '
|
|
'"canonical_name": "ConditionalExport", "module": "agents.submodule", '
|
|
'"name": "ConditionalExport"}], "modules": {"agents.submodule": {"optional_bindings": '
|
|
'{"LazyBinding": "binding_dependency"}, "optional_exports": '
|
|
'{"ConditionalExport": "export_dependency"}}}, "optional_dependencies": '
|
|
'{"binding_dependency": {"requirement": "binding-package>=1"}, '
|
|
'"export_dependency": {"extra": "export-extra"}}, "public_properties": '
|
|
'[{"class_name": "ConditionalExport", '
|
|
'"module": "agents.submodule", "names": ["status"]}, '
|
|
'{"factory_name": "create_client", '
|
|
'"module": "agents.submodule", "names": ["calls"]}], "public_class_contracts": '
|
|
'[{"abstract": false, "class_name": "ConditionalExport", '
|
|
'"module": "agents.submodule"}], "public_type_aliases": '
|
|
'[{"module": "agents.submodule", "name": "PublicAlias"}], '
|
|
'"public_typed_dicts": '
|
|
'[{"class_name": "ClientState", "module": "agents.submodule", '
|
|
'"names": ["status"]}]}',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
policy = load_submodule_export_policy(policy_path)
|
|
|
|
assert policy.modules == {
|
|
"agents.submodule": {
|
|
"optional_bindings": {"LazyBinding": "binding_dependency"},
|
|
"optional_exports": {"ConditionalExport": "export_dependency"},
|
|
}
|
|
}
|
|
assert [asdict(installation) for installation in policy.dependency_installations] == [
|
|
{
|
|
"dependency_module": "binding_dependency",
|
|
"extra": None,
|
|
"requirement": "binding-package>=1",
|
|
"unsupported_platforms": (),
|
|
},
|
|
{
|
|
"dependency_module": "export_dependency",
|
|
"extra": "export-extra",
|
|
"requirement": None,
|
|
"unsupported_platforms": (),
|
|
},
|
|
]
|
|
assert policy.canonical_imports == (
|
|
{
|
|
"canonical_module": "agents.submodule.impl",
|
|
"canonical_name": "ConditionalExport",
|
|
"module": "agents.submodule",
|
|
"name": "ConditionalExport",
|
|
},
|
|
)
|
|
assert policy.public_class_contracts == (
|
|
{
|
|
"abstract": False,
|
|
"class_name": "ConditionalExport",
|
|
"module": "agents.submodule",
|
|
},
|
|
)
|
|
assert tuple(
|
|
entry
|
|
for entry in policy.public_properties
|
|
if entry["module"]
|
|
not in {
|
|
"agents.testing.model",
|
|
"agents.testing.sandbox",
|
|
"agents.realtime.testing",
|
|
"agents.voice.testing",
|
|
}
|
|
) == (
|
|
{
|
|
"class_name": "ConditionalExport",
|
|
"module": "agents.submodule",
|
|
"names": ["status"],
|
|
},
|
|
{
|
|
"factory_name": "create_client",
|
|
"module": "agents.submodule",
|
|
"names": ["calls"],
|
|
},
|
|
)
|
|
assert policy.public_type_aliases == (
|
|
{
|
|
"module": "agents.submodule",
|
|
"name": "PublicAlias",
|
|
},
|
|
)
|
|
assert policy.public_typed_dicts == (
|
|
{
|
|
"class_name": "ClientState",
|
|
"module": "agents.submodule",
|
|
"names": ["status"],
|
|
},
|
|
)
|
|
|
|
|
|
def test_load_submodule_export_policy_rejects_invalid_class_abstract_state(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
policy_path = tmp_path / "policy.json"
|
|
policy_path.write_text(
|
|
'{"modules": {}, "optional_dependencies": {}, "public_class_contracts": '
|
|
'[{"abstract": "false", "class_name": "Released", "module": "agents"}]}',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="public_class_contracts abstract must be a boolean",
|
|
):
|
|
load_submodule_export_policy(policy_path)
|
|
|
|
|
|
def test_load_submodule_export_policy_collects_unsupported_platforms(tmp_path: Path) -> None:
|
|
policy_path = tmp_path / "policy.json"
|
|
policy_path.write_text(
|
|
'{"modules": {"agents.submodule": {"optional_exports": '
|
|
'{"ConditionalExport": "export_dependency"}}}, "optional_dependencies": '
|
|
'{"export_dependency": {"extra": "export-extra", '
|
|
'"unsupported_platforms": ["win32"]}}}',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
policy = load_submodule_export_policy(policy_path)
|
|
|
|
assert policy.dependency_installations[0].unsupported_platforms == ("win32",)
|
|
|
|
|
|
def test_repository_release_policy_declares_v020_contract_surfaces() -> None:
|
|
policy = load_submodule_export_policy(CONTRACT.with_name("released_api_contract_policy.json"))
|
|
|
|
assert next(
|
|
installation
|
|
for installation in policy.dependency_installations
|
|
if installation.dependency_module == "vercel"
|
|
).unsupported_platforms == ("win32",)
|
|
assert {(entry["module"], entry["name"]) for entry in policy.canonical_imports} >= {
|
|
("agents.items", "InputItem"),
|
|
("agents.extensions.sandbox", "ModalCloudBucketMountStrategy"),
|
|
("agents.extensions.sandbox", "ModalSandboxClient"),
|
|
("agents.extensions.sandbox", "ModalSandboxClientOptions"),
|
|
("agents.extensions.sandbox", "RunloopAfterIdle"),
|
|
("agents.extensions.sandbox", "RunloopGatewaySpec"),
|
|
("agents.extensions.sandbox", "RunloopLaunchParameters"),
|
|
("agents.extensions.sandbox", "RunloopMcpSpec"),
|
|
("agents.extensions.sandbox", "RunloopSandboxClient"),
|
|
("agents.extensions.sandbox", "RunloopSandboxClientOptions"),
|
|
("agents.extensions.sandbox", "RunloopTunnelConfig"),
|
|
("agents.extensions.sandbox", "RunloopUserParameters"),
|
|
("agents.extensions.sandbox", "VercelSandboxClient"),
|
|
("agents.extensions.sandbox", "VercelSandboxClientOptions"),
|
|
}
|
|
assert tuple(
|
|
entry
|
|
for entry in policy.public_properties
|
|
if entry["module"]
|
|
not in {
|
|
"agents.testing.model",
|
|
"agents.testing.sandbox",
|
|
"agents.realtime.testing",
|
|
"agents.voice.model",
|
|
"agents.voice.models.openai_model_provider",
|
|
"agents.voice.models.openai_stt",
|
|
"agents.voice.models.openai_tts",
|
|
"agents.voice.testing",
|
|
}
|
|
) == (
|
|
{
|
|
"class_name": "RunState",
|
|
"module": "agents.run_state",
|
|
"names": ["pending_input"],
|
|
},
|
|
{
|
|
"class_name": "RetryPolicyContext",
|
|
"module": "agents.retry",
|
|
"names": ["response_started", "replay_safety", "stateful_request"],
|
|
},
|
|
{
|
|
"class_name": "RunloopPlatformClient",
|
|
"module": "agents.extensions.sandbox",
|
|
"names": ["axons", "benchmarks", "blueprints", "network_policies", "secrets"],
|
|
},
|
|
{
|
|
"class_name": "RunloopSandboxClient",
|
|
"module": "agents.extensions.sandbox",
|
|
"names": ["platform"],
|
|
},
|
|
{
|
|
"class_name": "SandboxSessionState",
|
|
"module": "agents.sandbox.session.sandbox_session_state",
|
|
"names": ["mount_authority_redacted", "mount_authority_rebound"],
|
|
},
|
|
{
|
|
"class_name": "ScriptedSandboxSession",
|
|
"module": "agents.testing",
|
|
"names": ["calls", "remaining_steps"],
|
|
},
|
|
)
|
|
|
|
|
|
def test_repository_release_policy_declares_public_optional_modules() -> None:
|
|
policy = load_submodule_export_policy(CONTRACT.with_name("released_api_contract_policy.json"))
|
|
documented_voice_modules = {
|
|
"agents.voice.events",
|
|
"agents.voice.exceptions",
|
|
"agents.voice.input",
|
|
"agents.voice.imports",
|
|
"agents.voice.model",
|
|
"agents.voice.models.openai_model_provider",
|
|
"agents.voice.models.openai_stt",
|
|
"agents.voice.models.openai_tts",
|
|
"agents.voice.pipeline",
|
|
"agents.voice.pipeline_config",
|
|
"agents.voice.result",
|
|
"agents.voice.testing",
|
|
"agents.voice.utils",
|
|
"agents.voice.workflow",
|
|
}
|
|
expected_modules = documented_voice_modules | {
|
|
"agents.realtime.testing",
|
|
"agents.testing",
|
|
"agents.testing.model",
|
|
"agents.testing.sandbox",
|
|
"agents.voice",
|
|
}
|
|
|
|
documented_directive_modules = {
|
|
line.removeprefix("::: ")
|
|
for path in (CONTRACT.parents[2] / "docs" / "ref" / "voice").rglob("*.md")
|
|
for line in path.read_text(encoding="utf-8").splitlines()
|
|
if line.startswith("::: agents.voice")
|
|
}
|
|
|
|
assert documented_directive_modules == documented_voice_modules
|
|
assert expected_modules <= policy.modules.keys()
|
|
for module_name in documented_voice_modules - {
|
|
"agents.voice.imports",
|
|
"agents.voice.testing",
|
|
}:
|
|
assert policy.modules[module_name] == {
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
for module_name in ("agents.voice", "agents.voice.imports", "agents.voice.testing"):
|
|
assert policy.modules[module_name] == {
|
|
"optional_bindings": {
|
|
export: "numpy" for export in importlib.import_module(module_name).__all__
|
|
},
|
|
"optional_exports": {},
|
|
}
|
|
assert (
|
|
next(
|
|
installation
|
|
for installation in policy.dependency_installations
|
|
if installation.dependency_module == "numpy"
|
|
).extra
|
|
== "voice"
|
|
)
|
|
|
|
|
|
def test_repository_release_policy_declares_public_state_surfaces() -> None:
|
|
policy = load_submodule_export_policy(CONTRACT.with_name("released_api_contract_policy.json"))
|
|
expected_modules = {
|
|
"agents.realtime.testing",
|
|
"agents.testing",
|
|
"agents.testing.model",
|
|
"agents.testing.sandbox",
|
|
"agents.voice.testing",
|
|
}
|
|
|
|
assert tuple(
|
|
entry
|
|
for entry in policy.public_properties
|
|
if entry["module"]
|
|
in {
|
|
"agents.testing.model",
|
|
"agents.testing.sandbox",
|
|
"agents.realtime.testing",
|
|
"agents.voice.testing",
|
|
}
|
|
) == (
|
|
{
|
|
"class_name": "ScriptedModel",
|
|
"module": "agents.testing.model",
|
|
"names": ["calls", "remaining_steps", "first_call", "last_call"],
|
|
},
|
|
{
|
|
"class_name": "ScriptedRealtimeModel",
|
|
"module": "agents.realtime.testing",
|
|
"names": ["listeners", "connect_calls", "sent_events", "remaining_steps"],
|
|
},
|
|
{
|
|
"factory_name": "scripted_sandbox_session",
|
|
"module": "agents.testing.sandbox",
|
|
"names": ["calls", "remaining_steps"],
|
|
},
|
|
{
|
|
"class_name": "ScriptedSTTModel",
|
|
"module": "agents.voice.testing",
|
|
"names": ["calls", "session_calls", "created_sessions"],
|
|
},
|
|
{
|
|
"class_name": "ScriptedTTSModel",
|
|
"module": "agents.voice.testing",
|
|
"names": ["calls"],
|
|
},
|
|
{
|
|
"class_name": "ScriptedVoiceWorkflow",
|
|
"module": "agents.voice.testing",
|
|
"names": ["transcriptions"],
|
|
},
|
|
)
|
|
assert tuple(
|
|
entry
|
|
for entry in policy.public_properties
|
|
if entry["module"]
|
|
in {
|
|
"agents.voice.model",
|
|
"agents.voice.models.openai_model_provider",
|
|
"agents.voice.models.openai_stt",
|
|
"agents.voice.models.openai_tts",
|
|
}
|
|
) == (
|
|
{
|
|
"class_name": "STTModel",
|
|
"module": "agents.voice.model",
|
|
"names": ["model_name"],
|
|
},
|
|
{
|
|
"class_name": "TTSModel",
|
|
"module": "agents.voice.model",
|
|
"names": ["model_name"],
|
|
},
|
|
{
|
|
"class_name": "OpenAISTTModel",
|
|
"module": "agents.voice.models.openai_stt",
|
|
"names": ["model_name"],
|
|
},
|
|
{
|
|
"class_name": "OpenAITTSModel",
|
|
"module": "agents.voice.models.openai_tts",
|
|
"names": ["model_name"],
|
|
},
|
|
{
|
|
"class_name": "OpenAIVoiceModelProvider",
|
|
"module": "agents.voice.models.openai_model_provider",
|
|
"names": ["agent_registration"],
|
|
},
|
|
)
|
|
assert policy.public_class_contracts == (
|
|
{
|
|
"class_name": "STTModel",
|
|
"module": "agents.voice.model",
|
|
"abstract_members": ["create_session", "model_name", "transcribe"],
|
|
},
|
|
{
|
|
"class_name": "StreamedTranscriptionSession",
|
|
"module": "agents.voice.model",
|
|
"abstract_members": ["close", "transcribe_turns"],
|
|
},
|
|
{
|
|
"class_name": "TTSModel",
|
|
"module": "agents.voice.model",
|
|
"abstract_members": ["model_name", "run"],
|
|
},
|
|
{
|
|
"class_name": "VoiceModelProvider",
|
|
"module": "agents.voice.model",
|
|
"abstract_members": ["get_stt_model", "get_tts_model"],
|
|
},
|
|
{
|
|
"abstract": False,
|
|
"class_name": "OpenAISTTModel",
|
|
"module": "agents.voice.models.openai_stt",
|
|
},
|
|
{
|
|
"abstract": False,
|
|
"class_name": "OpenAISTTTranscriptionSession",
|
|
"module": "agents.voice.models.openai_stt",
|
|
},
|
|
{
|
|
"abstract": False,
|
|
"class_name": "OpenAITTSModel",
|
|
"module": "agents.voice.models.openai_tts",
|
|
},
|
|
{
|
|
"class_name": "VoiceWorkflowBase",
|
|
"module": "agents.voice.workflow",
|
|
"abstract_members": ["run"],
|
|
},
|
|
)
|
|
assert policy.public_type_aliases == (
|
|
{
|
|
"module": "agents.voice.model",
|
|
"name": "TTSVoice",
|
|
},
|
|
{
|
|
"module": "agents.voice.events",
|
|
"name": "VoiceStreamEvent",
|
|
},
|
|
)
|
|
type_aliases: dict[tuple[str, str], dict[str, Any]] = {
|
|
(cast(str, entry["module"]), cast(str, entry["name"])): cast(
|
|
dict[str, Any], entry["definition"]
|
|
)
|
|
for entry in contract_support._public_type_alias_contract(policy.public_type_aliases, None)
|
|
}
|
|
tts_voice = type_aliases[("agents.voice.model", "TTSVoice")]
|
|
assert tts_voice["kind"] == "union"
|
|
assert [
|
|
value["value"]
|
|
for member in tts_voice["members"]
|
|
if member["kind"] == "literal"
|
|
for value in member["values"]
|
|
] == [
|
|
"alloy",
|
|
"ash",
|
|
"ballad",
|
|
"cedar",
|
|
"coral",
|
|
"echo",
|
|
"fable",
|
|
"marin",
|
|
"nova",
|
|
"onyx",
|
|
"sage",
|
|
"shimmer",
|
|
"verse",
|
|
]
|
|
assert {member["identity"] for member in tts_voice["members"] if member["kind"] == "type"} == {
|
|
"agents.voice.model.TTSCustomVoice"
|
|
}
|
|
voice_stream_event = type_aliases[("agents.voice.events", "VoiceStreamEvent")]
|
|
assert voice_stream_event["kind"] == "union"
|
|
assert {member["identity"] for member in voice_stream_event["members"]} == {
|
|
"agents.voice.events.VoiceStreamEventAudio",
|
|
"agents.voice.events.VoiceStreamEventError",
|
|
"agents.voice.events.VoiceStreamEventLifecycle",
|
|
}
|
|
assert policy.public_typed_dicts == (
|
|
{
|
|
"class_name": "ModelStepSpec",
|
|
"module": "agents.testing.model",
|
|
"names": [
|
|
"output",
|
|
"usage",
|
|
"response_id",
|
|
"request_id",
|
|
"raw_usage",
|
|
"error",
|
|
"responder",
|
|
"stream_events",
|
|
"retry_advice",
|
|
],
|
|
},
|
|
{
|
|
"class_name": "SandboxStepSpec",
|
|
"module": "agents.testing.sandbox",
|
|
"names": ["method", "match", "result", "responder", "error"],
|
|
},
|
|
{
|
|
"class_name": "RealtimeConnectCall",
|
|
"module": "agents.realtime.testing",
|
|
"names": [
|
|
"api_key_provided",
|
|
"headers_provided",
|
|
"url",
|
|
"initial_model_settings",
|
|
"playback_tracker",
|
|
"call_id",
|
|
],
|
|
},
|
|
{
|
|
"class_name": "TTSCustomVoice",
|
|
"module": "agents.voice",
|
|
"names": ["id"],
|
|
},
|
|
)
|
|
for module_name in expected_modules:
|
|
module = importlib.import_module(module_name)
|
|
assert module.__all__
|
|
assert all(type(export) is str for export in module.__all__)
|
|
|
|
expected_canonical_imports = {
|
|
("agents.testing", name, "agents.testing.model", name)
|
|
for name in importlib.import_module("agents.testing.model").__all__
|
|
} | {
|
|
("agents.testing", name, "agents.testing.sandbox", name)
|
|
for name in importlib.import_module("agents.testing.sandbox").__all__
|
|
}
|
|
actual_canonical_imports = {
|
|
(
|
|
entry["module"],
|
|
entry["name"],
|
|
entry["canonical_module"],
|
|
entry["canonical_name"],
|
|
)
|
|
for entry in policy.canonical_imports
|
|
if entry["module"] == "agents.testing"
|
|
}
|
|
|
|
assert actual_canonical_imports == expected_canonical_imports
|
|
for module_name, name, canonical_module_name, canonical_name in actual_canonical_imports:
|
|
module = importlib.import_module(module_name)
|
|
canonical_module = importlib.import_module(canonical_module_name)
|
|
assert getattr(module, name) is getattr(canonical_module, canonical_name)
|
|
|
|
voice_canonical_modules = {
|
|
"AudioInput": "agents.voice.input",
|
|
"StreamedAudioInput": "agents.voice.input",
|
|
"STTModel": "agents.voice.model",
|
|
"STTModelSettings": "agents.voice.model",
|
|
"TTSCustomVoice": "agents.voice.model",
|
|
"TTSModel": "agents.voice.model",
|
|
"TTSModelSettings": "agents.voice.model",
|
|
"TTSVoice": "agents.voice.model",
|
|
"VoiceModelProvider": "agents.voice.model",
|
|
"StreamedAudioResult": "agents.voice.result",
|
|
"SingleAgentVoiceWorkflow": "agents.voice.workflow",
|
|
"OpenAIVoiceModelProvider": "agents.voice.models.openai_model_provider",
|
|
"OpenAISTTModel": "agents.voice.models.openai_stt",
|
|
"OpenAITTSModel": "agents.voice.models.openai_tts",
|
|
"VoiceStreamEventAudio": "agents.voice.events",
|
|
"VoiceStreamEventError": "agents.voice.events",
|
|
"VoiceStreamEventLifecycle": "agents.voice.events",
|
|
"VoiceStreamEvent": "agents.voice.events",
|
|
"VoicePipeline": "agents.voice.pipeline",
|
|
"VoicePipelineConfig": "agents.voice.pipeline_config",
|
|
"get_sentence_based_splitter": "agents.voice.utils",
|
|
"VoiceWorkflowHelper": "agents.voice.workflow",
|
|
"VoiceWorkflowBase": "agents.voice.workflow",
|
|
"SingleAgentWorkflowCallbacks": "agents.voice.workflow",
|
|
"StreamedTranscriptionSession": "agents.voice.model",
|
|
"OpenAISTTTranscriptionSession": "agents.voice.models.openai_stt",
|
|
"STTWebsocketConnectionError": "agents.voice.exceptions",
|
|
}
|
|
expected_voice_canonical_imports = {
|
|
("agents.voice", name, canonical_module_name, name)
|
|
for name, canonical_module_name in voice_canonical_modules.items()
|
|
}
|
|
actual_voice_canonical_imports = {
|
|
(
|
|
entry["module"],
|
|
entry["name"],
|
|
entry["canonical_module"],
|
|
entry["canonical_name"],
|
|
)
|
|
for entry in policy.canonical_imports
|
|
if entry["module"] == "agents.voice"
|
|
}
|
|
|
|
assert actual_voice_canonical_imports == expected_voice_canonical_imports
|
|
for module_name, name, canonical_module_name, canonical_name in actual_voice_canonical_imports:
|
|
module = importlib.import_module(module_name)
|
|
canonical_module = importlib.import_module(canonical_module_name)
|
|
assert getattr(module, name) is getattr(canonical_module, canonical_name)
|
|
|
|
|
|
def test_voice_testing_start_sentinel_has_stable_contract_identity() -> None:
|
|
from agents.voice.testing import _START_NOT_CONFIGURED
|
|
|
|
assert _default_contract(_START_NOT_CONFIGURED) == {
|
|
"kind": "sentinel",
|
|
"identity": "agents.voice.testing._START_NOT_CONFIGURED",
|
|
}
|
|
with pytest.raises(TypeError, match="Unsupported public API default value: builtins.object"):
|
|
_default_contract(object())
|
|
|
|
|
|
def test_default_contract_does_not_import_voice_testing_for_unrelated_defaults(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
original_import = builtins.__import__
|
|
|
|
def guarded_import(name: str, *args: Any, **kwargs: Any) -> Any:
|
|
if name == "agents.voice.testing":
|
|
raise AssertionError("Unrelated defaults must not import the optional Voice package.")
|
|
return original_import(name, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", guarded_import)
|
|
|
|
assert _default_contract(()) == {
|
|
"kind": "sequence",
|
|
"type": "builtins.tuple",
|
|
"items": [],
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"unsupported_platforms",
|
|
["win32", [""], ["win32", "win32"]],
|
|
)
|
|
def test_load_submodule_export_policy_rejects_invalid_unsupported_platforms(
|
|
tmp_path: Path, unsupported_platforms: object
|
|
) -> None:
|
|
policy_path = tmp_path / "policy.json"
|
|
policy_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"modules": {
|
|
"agents.submodule": {
|
|
"optional_exports": {"ConditionalExport": "export_dependency"}
|
|
}
|
|
},
|
|
"optional_dependencies": {
|
|
"export_dependency": {
|
|
"extra": "export-extra",
|
|
"unsupported_platforms": unsupported_platforms,
|
|
}
|
|
},
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="must be a list of unique non-empty strings"):
|
|
load_submodule_export_policy(policy_path)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"protected_path",
|
|
[
|
|
CONTRACT,
|
|
CONTRACT.with_name("released_api_contract_policy.json"),
|
|
],
|
|
)
|
|
def test_prospective_output_rejects_contract_input_path(protected_path: Path) -> None:
|
|
root = CONTRACT.parents[2]
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(root / ".github" / "scripts" / "update_released_api_contract.py"),
|
|
"--version",
|
|
version("openai-agents"),
|
|
"--output",
|
|
str(protected_path),
|
|
],
|
|
cwd=root,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
assert result.returncode == 1
|
|
assert result.stderr.strip() == (
|
|
"--output must not overwrite released API contract inputs: "
|
|
"tests/fixtures/released_api_contract.json or "
|
|
"tests/fixtures/released_api_contract_policy.json"
|
|
)
|
|
|
|
|
|
def test_public_api_contract_allows_declared_optional_submodule_binding(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["OptionalBackend"])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {"OptionalBackend": "missing_optional_backend_dependency"},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == []
|
|
|
|
|
|
def test_public_api_contract_skips_fully_optional_unimportable_submodule(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.optional_submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.optional_submodule": {
|
|
"names": ["OptionalClient", "OptionalConfig"],
|
|
"optional_bindings": {
|
|
"OptionalClient": "missing_optional_dependency",
|
|
"OptionalConfig": "missing_optional_dependency",
|
|
},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
def import_module(module_name: str, _agents_module: object) -> object:
|
|
if module_name == "agents":
|
|
return agents_module
|
|
raise ImportError("The optional dependency is unavailable.")
|
|
|
|
monkeypatch.setattr(contract_support, "_import_contract_module", import_module)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == []
|
|
|
|
|
|
def test_public_api_contract_allows_declared_optional_submodule_export(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "missing_optional_backend_dependency"},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == []
|
|
|
|
|
|
def test_public_api_contract_rejects_optional_export_that_remains_in_all(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["OptionalBackend"])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "missing_optional_backend_dependency"},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Invalid released agents.submodule optional dependency declaration: "
|
|
"'OptionalBackend' remains in __all__ but its binding is unavailable; "
|
|
"declare it in optional_bindings instead of optional_exports"
|
|
]
|
|
|
|
|
|
def test_public_api_contract_rejects_optional_binding_absent_from_all(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {"OptionalBackend": "missing_optional_backend_dependency"},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Invalid released agents.submodule optional dependency declaration: "
|
|
"'OptionalBackend' is absent from __all__; declare it in optional_exports "
|
|
"instead of optional_bindings",
|
|
"Missing released agents.submodule exports: ['OptionalBackend']",
|
|
]
|
|
|
|
|
|
def test_public_api_contract_requires_available_optional_submodule_export(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": "json"},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Missing released agents.submodule exports: ['OptionalBackend']",
|
|
"Missing released agents.submodule bindings: ['OptionalBackend']",
|
|
]
|
|
|
|
|
|
def test_public_api_contract_treats_loaded_dependency_without_spec_as_available(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
dependency_name = "loaded_dependency_without_spec"
|
|
monkeypatch.setitem(sys.modules, dependency_name, SimpleNamespace(__spec__=None))
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {"OptionalBackend": dependency_name},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Missing released agents.submodule exports: ['OptionalBackend']",
|
|
"Missing released agents.submodule bindings: ['OptionalBackend']",
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("optional_exports", "expected_error"),
|
|
[
|
|
(
|
|
{"OptionalBackend": None},
|
|
"optional_exports dependency for 'OptionalBackend' must be a non-empty string",
|
|
),
|
|
(
|
|
{"OptionalBackend": ""},
|
|
"optional_exports dependency for 'OptionalBackend' must be a non-empty string",
|
|
),
|
|
(
|
|
[],
|
|
"optional_exports must be an object mapping export names to dependency modules",
|
|
),
|
|
],
|
|
)
|
|
def test_public_api_contract_rejects_malformed_optional_dependency_declarations(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
optional_exports: object,
|
|
expected_error: str,
|
|
) -> None:
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=[])
|
|
contract: dict[str, Any] = {
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["OptionalBackend"],
|
|
"optional_bindings": {},
|
|
"optional_exports": optional_exports,
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Invalid released agents.submodule optional dependency declarations: " + expected_error
|
|
]
|
|
|
|
|
|
def test_release_contract_update_rejects_new_submodule_export_without_binding(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
existing = object()
|
|
agents_module = SimpleNamespace(__all__=[])
|
|
submodule = SimpleNamespace(__all__=["Existing", "Added"], Existing=existing)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": ["agents.submodule"],
|
|
"required_submodule_exports": {
|
|
"agents.submodule": {
|
|
"names": ["Existing"],
|
|
"optional_bindings": {},
|
|
"optional_exports": {},
|
|
}
|
|
},
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
monkeypatch.setattr(
|
|
contract_support,
|
|
"_import_contract_module",
|
|
lambda module_name, _agents_module: (
|
|
agents_module if module_name == "agents" else submodule
|
|
),
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Cannot promote an invalid released API contract",
|
|
):
|
|
build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
|
|
def test_release_contract_update_rejects_incompatible_current_surface() -> None:
|
|
class Released:
|
|
def __init__(self, value: str) -> None:
|
|
self.value = value
|
|
|
|
class Incompatible:
|
|
def __init__(self, renamed: str) -> None:
|
|
self.renamed = renamed
|
|
|
|
agents_module = SimpleNamespace(__all__=["Released"], Released=Incompatible)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": ["Released"],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"callables": {"Released": _callable_contract(Released)},
|
|
}
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Cannot promote an incompatible released API contract",
|
|
):
|
|
build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
|
|
def test_release_contract_update_rejects_function_signature_change() -> None:
|
|
def released(value: str, optional: int = 1) -> None:
|
|
_ = (value, optional)
|
|
|
|
def incompatible(renamed: str, optional: int = 1) -> None:
|
|
_ = (renamed, optional)
|
|
|
|
agents_module = SimpleNamespace(__all__=["released"], released=incompatible)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": ["released"],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {"released": _callable_contract(released)},
|
|
}
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"released changed its released positional parameter prefix: expected "
|
|
"[{'name': 'value', 'kind': 'POSITIONAL_OR_KEYWORD', "
|
|
"'default': {'kind': 'required'}}, {'name': 'optional', "
|
|
"'kind': 'POSITIONAL_OR_KEYWORD', "
|
|
"'default': {'kind': 'literal', 'type': 'builtins.int', 'value': 1}}], got "
|
|
"[{'name': 'renamed', 'kind': 'POSITIONAL_OR_KEYWORD', "
|
|
"'default': {'kind': 'required'}}, {'name': 'optional', "
|
|
"'kind': 'POSITIONAL_OR_KEYWORD', "
|
|
"'default': {'kind': 'literal', 'type': 'builtins.int', 'value': 1}}]",
|
|
"released.renamed added a required parameter",
|
|
]
|
|
|
|
|
|
def test_release_contract_update_rejects_class_replaced_by_function() -> None:
|
|
class Released:
|
|
def __init__(self, value: str) -> None:
|
|
self.value = value
|
|
|
|
def replacement(value: str) -> None:
|
|
_ = value
|
|
|
|
agents_module = SimpleNamespace(__all__=["Released"], Released=replacement)
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": ["Released"],
|
|
"public_modules": ["agents"],
|
|
"canonical_imports": [],
|
|
"callables": {"Released": _callable_contract(Released)},
|
|
}
|
|
|
|
assert validate_released_api_contract(contract, agents_module=agents_module) == [
|
|
"Released callable agents.Released changed kind from class to function"
|
|
]
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Released callable agents.Released changed kind from class to function",
|
|
):
|
|
build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|
|
|
|
|
|
def test_release_contract_update_rejects_duplicate_exports() -> None:
|
|
agents_module = SimpleNamespace(__all__=["Duplicate", "Duplicate"], Duplicate=object())
|
|
contract: dict[str, Any] = {
|
|
"baseline": "v0.19.4",
|
|
"baseline_commit": "a" * 40,
|
|
"required_top_level_exports": [],
|
|
"public_modules": [],
|
|
"canonical_imports": [],
|
|
"callables": {},
|
|
}
|
|
|
|
with pytest.raises(ValueError, match="must not contain duplicate exports"):
|
|
build_released_api_contract(
|
|
contract,
|
|
baseline="v0.20.0",
|
|
baseline_commit="b" * 40,
|
|
agents_module=agents_module,
|
|
)
|