triagesieve_env / tests /test_packaging.py
Angshuman28's picture
Upload folder using huggingface_hub
b89c8aa verified
"""Tests for runtime and packaging surface (Phase 5).
Covers:
- server/app.py: create_app wiring produces a FastAPI instance
- client.py: TriageSieveEnv subclasses EnvClient with required abstract methods
- openenv.yaml: manifest fields match CLAUDE.md §5.4
- pyproject.toml: metadata and dependencies match CLAUDE.md §26
- __init__.py: exports include TriageSieveEnv
- server/Dockerfile: CMD targets the correct module path
"""
from __future__ import annotations
import importlib
import pathlib
import sys
from typing import Any, Dict
from unittest.mock import MagicMock, patch
import pytest
import yaml
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
ROOT = pathlib.Path(__file__).resolve().parent.parent
SERVER_DIR = ROOT / "server"
# ---------------------------------------------------------------------------
# 1. server/app.py — create_app wiring
# ---------------------------------------------------------------------------
class TestAppModule:
"""Tests for server/app.py."""
def test_app_module_imports(self) -> None:
"""app.py must be importable."""
from ..server import app # noqa: F401
def test_app_is_fastapi_instance(self) -> None:
"""Module-level `app` must be a FastAPI instance."""
from fastapi import FastAPI
from ..server.app import app
assert isinstance(app, FastAPI)
def test_app_created_with_correct_env_class(self) -> None:
"""create_app must be called with TriageSieveEnvironment class (factory)."""
from ..server.triagesieve_env_environment import (
TriageSieveEnvironment,
)
# Re-import module with patched create_app to capture args
captured: dict[str, Any] = {}
original_create_app = None
def spy_create_app(**kwargs: Any) -> MagicMock:
captured.update(kwargs)
nonlocal original_create_app
# Return a mock FastAPI to avoid side effects
return MagicMock()
with patch(
"openenv.core.env_server.http_server.create_app",
side_effect=spy_create_app,
):
# Force reimport
mod_name = "triagesieve_env.server.app"
if mod_name in sys.modules:
del sys.modules[mod_name]
importlib.import_module(mod_name)
assert captured.get("env") is TriageSieveEnvironment
def test_app_created_with_correct_action_cls(self) -> None:
"""create_app must receive TriageSieveAction as action_cls."""
from ..models import TriageSieveAction
captured: dict[str, Any] = {}
def spy_create_app(**kwargs: Any) -> MagicMock:
captured.update(kwargs)
return MagicMock()
with patch(
"openenv.core.env_server.http_server.create_app",
side_effect=spy_create_app,
):
mod_name = "triagesieve_env.server.app"
if mod_name in sys.modules:
del sys.modules[mod_name]
importlib.import_module(mod_name)
assert captured.get("action_cls") is TriageSieveAction
def test_app_created_with_correct_observation_cls(self) -> None:
"""create_app must receive TriageSieveObservation as observation_cls."""
from ..models import TriageSieveObservation
captured: dict[str, Any] = {}
def spy_create_app(**kwargs: Any) -> MagicMock:
captured.update(kwargs)
return MagicMock()
with patch(
"openenv.core.env_server.http_server.create_app",
side_effect=spy_create_app,
):
mod_name = "triagesieve_env.server.app"
if mod_name in sys.modules:
del sys.modules[mod_name]
importlib.import_module(mod_name)
assert captured.get("observation_cls") is TriageSieveObservation
def test_app_env_name(self) -> None:
"""create_app must use env_name='.'."""
captured: dict[str, Any] = {}
def spy_create_app(**kwargs: Any) -> MagicMock:
captured.update(kwargs)
return MagicMock()
with patch(
"openenv.core.env_server.http_server.create_app",
side_effect=spy_create_app,
):
mod_name = "triagesieve_env.server.app"
if mod_name in sys.modules:
del sys.modules[mod_name]
importlib.import_module(mod_name)
assert captured.get("env_name") == "triagesieve_env"
def test_app_max_concurrent_envs(self) -> None:
"""create_app must set max_concurrent_envs=4."""
captured: dict[str, Any] = {}
def spy_create_app(**kwargs: Any) -> MagicMock:
captured.update(kwargs)
return MagicMock()
with patch(
"openenv.core.env_server.http_server.create_app",
side_effect=spy_create_app,
):
mod_name = "triagesieve_env.server.app"
if mod_name in sys.modules:
del sys.modules[mod_name]
importlib.import_module(mod_name)
assert captured.get("max_concurrent_envs") == 4
# ---------------------------------------------------------------------------
# 2. client.py — EnvClient subclass
# ---------------------------------------------------------------------------
class TestClientModule:
"""Tests for client.py."""
def test_client_module_imports(self) -> None:
"""client.py must be importable."""
from .. import client # noqa: F401
def test_client_subclasses_envclient(self) -> None:
"""TriageSieveEnv must subclass EnvClient."""
from openenv.core.env_client import EnvClient
from ..client import TriageSieveEnv
assert issubclass(TriageSieveEnv, EnvClient)
def test_step_payload_returns_dict(self) -> None:
"""_step_payload must return a dict from action.model_dump(exclude_none=True)."""
from ..client import TriageSieveEnv
from ..models import ActionType, TriageSieveAction
# Create client without connecting
client = object.__new__(TriageSieveEnv)
action = TriageSieveAction(
action_type=ActionType.OPEN_TICKET,
ticket_id="T001",
metadata={},
)
payload = client._step_payload(action)
assert isinstance(payload, dict)
assert payload["action_type"] == "open_ticket"
assert payload["ticket_id"] == "T001"
# None fields should be excluded
assert "issue_family" not in payload
def test_parse_result_returns_step_result(self) -> None:
"""_parse_result must return a StepResult with correct observation."""
from openenv.core.env_client import StepResult
from ..client import TriageSieveEnv
client = object.__new__(TriageSieveEnv)
# Minimal valid observation payload
payload: Dict[str, Any] = {
"observation": {
"done": False,
"reward": 0.01,
"metadata": {},
"inbox_summaries": [],
"focused_ticket": None,
"available_templates": [],
"allowed_queues": [],
"routing_policy_cards": [],
"sla_policy_cards": [],
"legal_actions": ["open_ticket"],
"action_budget_remaining": 10,
"step_count": 1,
"current_time": "2026-01-01T00:00:00",
"last_action_result": "ok",
"task_difficulty": "easy",
"hint": None,
},
"reward": 0.01,
"done": False,
}
result = client._parse_result(payload)
assert isinstance(result, StepResult)
assert result.reward == 0.01
assert result.done is False
assert result.observation.step_count == 1
def test_parse_state_returns_state(self) -> None:
"""_parse_state must return a TriageSieveState."""
from ..client import TriageSieveEnv
from ..models import TriageSieveState
client = object.__new__(TriageSieveEnv)
payload: Dict[str, Any] = {
"episode_id": "ep-001",
"step_count": 3,
"task_difficulty": "medium",
"seed": 42,
"total_tickets": 2,
"action_budget": 8,
"action_budget_remaining": 5,
"mode": "eval_strict",
"tickets_summary": [],
}
state = client._parse_state(payload)
assert isinstance(state, TriageSieveState)
assert state.episode_id == "ep-001"
assert state.step_count == 3
assert state.seed == 42
def test_step_payload_excludes_none_fields(self) -> None:
"""_step_payload must not include None optional fields."""
from ..client import TriageSieveEnv
from ..models import ActionType, TriageSieveAction
client = object.__new__(TriageSieveEnv)
action = TriageSieveAction(
action_type=ActionType.SKIP_TURN,
metadata={},
)
payload = client._step_payload(action)
assert "ticket_id" not in payload
assert "issue_family" not in payload
assert "queue_id" not in payload
# ---------------------------------------------------------------------------
# 3. openenv.yaml — manifest
# ---------------------------------------------------------------------------
'''
class TestOpenenvYaml:
"""Tests for openenv.yaml matching CLAUDE.md §5.4."""
@pytest.fixture()
def manifest(self) -> dict[str, Any]:
yaml_path = ROOT / "openenv.yaml"
assert yaml_path.exists(), f"openenv.yaml not found at {yaml_path}"
with open(yaml_path, encoding="utf-8") as f:
return yaml.safe_load(f)
def test_name(self, manifest: dict[str, Any]) -> None:
assert manifest["name"] == "triagesieve_env"
def test_version(self, manifest: dict[str, Any]) -> None:
assert manifest["version"] == "0.1.0"
def test_entry_point(self, manifest: dict[str, Any]) -> None:
assert (
manifest["entry_point"]
== "triagesieve_env.server.triagesieve_env_environment:TriageSieveEnvironment"
)
def test_action_class(self, manifest: dict[str, Any]) -> None:
assert manifest["action_class"] == "triagesieve_env.models:TriageSieveAction"
def test_observation_class(self, manifest: dict[str, Any]) -> None:
assert manifest["observation_class"] == "triagesieve_env.models:TriageSieveObservation"
'''
# ---------------------------------------------------------------------------
# 4. pyproject.toml — metadata
# ---------------------------------------------------------------------------
'''
class TestPyprojectToml:
"""Tests for pyproject.toml matching CLAUDE.md §26."""
@pytest.fixture()
def toml_data(self) -> dict[str, Any]:
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib # type: ignore[no-redef]
toml_path = ROOT / "pyproject.toml"
assert toml_path.exists()
with open(toml_path, "rb") as f:
return tomllib.load(f)
def test_project_name(self, toml_data: dict[str, Any]) -> None:
assert toml_data["project"]["name"] == "openenv-triagesieve-env"
def test_project_version(self, toml_data: dict[str, Any]) -> None:
assert toml_data["project"]["version"] == "0.1.0"
def test_requires_python(self, toml_data: dict[str, Any]) -> None:
assert toml_data["project"]["requires-python"] == ">=3.11"
def test_core_dependencies(self, toml_data: dict[str, Any]) -> None:
deps = toml_data["project"]["dependencies"]
dep_names = [d.split(">=")[0].split(">")[0].split("==")[0] for d in deps]
for required in ["openenv-core[core]", "pydantic", "fastapi", "uvicorn"]:
assert required in dep_names, f"Missing dependency: {required}"
def test_dev_dependencies(self, toml_data: dict[str, Any]) -> None:
dev = toml_data["project"]["optional-dependencies"]["dev"]
dev_names = [d.split(">=")[0] for d in dev]
assert "pytest" in dev_names
assert "pytest-asyncio" in dev_names
def test_black_target_version(self, toml_data: dict[str, Any]) -> None:
"""black target-version must include py311, not py312."""
target = toml_data["tool"]["black"]["target-version"]
assert "py311" in target
assert "py312" not in target
'''
# ---------------------------------------------------------------------------
# 5. __init__.py — exports
# ---------------------------------------------------------------------------
class TestInitExports:
"""Tests for __init__.py package exports."""
def test_exports_action(self) -> None:
from ..models import TriageSieveAction # noqa: F401
def test_exports_observation(self) -> None:
from ..models import TriageSieveObservation # noqa: F401
def test_exports_state(self) -> None:
from ..models import TriageSieveState # noqa: F401
def test_exports_client(self) -> None:
from ..client import TriageSieveEnv # noqa: F401
# ---------------------------------------------------------------------------
# 6. server/Dockerfile — CMD alignment
# ---------------------------------------------------------------------------
'''
class TestDockerfile:
"""Tests for server/Dockerfile matching CLAUDE.md §27."""
@pytest.fixture()
def dockerfile_content(self) -> str:
path = SERVER_DIR / "Dockerfile"
assert path.exists()
return path.read_text()
def test_base_image(self, dockerfile_content: str) -> None:
assert "python:3.11-slim" in dockerfile_content
def test_cmd_module_path(self, dockerfile_content: str) -> None:
assert "triagesieve_env.server.app:app" in dockerfile_content
def test_exposed_port(self, dockerfile_content: str) -> None:
assert "EXPOSE 8000" in dockerfile_content
'''
# ---------------------------------------------------------------------------
# 7. server/requirements.txt — deps present
# ---------------------------------------------------------------------------
class TestRequirementsTxt:
"""Tests for server/requirements.txt."""
@pytest.fixture()
def requirements(self) -> list[str]:
path = SERVER_DIR / "requirements.txt"
assert path.exists()
lines = path.read_text(encoding="utf-8").strip().splitlines()
return [l.strip() for l in lines if l.strip() and not l.startswith("#")]
def test_has_openenv_core(self, requirements: list[str]) -> None:
assert any("openenv-core" in r for r in requirements)
def test_has_pydantic(self, requirements: list[str]) -> None:
assert any("pydantic" in r for r in requirements)
def test_has_fastapi(self, requirements: list[str]) -> None:
assert any("fastapi" in r for r in requirements)
def test_has_uvicorn(self, requirements: list[str]) -> None:
assert any("uvicorn" in r for r in requirements)