| |
| |
| |
| |
| |
|
|
| """Tests for the openenv validate command and runtime validation utilities.""" |
|
|
| from __future__ import annotations |
|
|
| import json |
| from pathlib import Path |
| from unittest.mock import patch |
|
|
| from openenv.cli.__main__ import app |
| from openenv.cli._validation import validate_running_environment |
| from typer.testing import CliRunner |
|
|
|
|
| runner = CliRunner() |
|
|
|
|
| class _MockResponse: |
| """Minimal mock response object for requests.get/post tests.""" |
|
|
| def __init__(self, status_code: int, payload: dict | None = None): |
| self.status_code = status_code |
| self._payload = payload |
|
|
| def json(self) -> dict: |
| if self._payload is None: |
| raise ValueError("No JSON payload") |
| return self._payload |
|
|
|
|
| def _write_minimal_valid_env(env_dir: Path) -> None: |
| """Create a minimal local environment that passes local validation.""" |
| (env_dir / "server").mkdir(parents=True) |
|
|
| (env_dir / "openenv.yaml").write_text( |
| "spec_version: 1\nname: test_env\ntype: space\nruntime: fastapi\napp: server.app:app\nport: 8000\n" |
| ) |
| (env_dir / "uv.lock").write_text("") |
| (env_dir / "pyproject.toml").write_text( |
| "[project]\n" |
| 'name = "test-env"\n' |
| 'version = "0.1.0"\n' |
| 'dependencies = ["openenv-core>=0.2.0"]\n' |
| "\n" |
| "[project.scripts]\n" |
| 'server = "server.app:main"\n' |
| ) |
| (env_dir / "server" / "app.py").write_text( |
| "def main():\n return None\n\nif __name__ == '__main__':\n main()\n" |
| ) |
|
|
|
|
| def test_validate_running_environment_success() -> None: |
| """Runtime validator returns passing criteria for a conforming server.""" |
|
|
| def _fake_get(url: str, timeout: float) -> _MockResponse: |
| if url.endswith("/openapi.json"): |
| return _MockResponse( |
| 200, |
| { |
| "info": {"version": "1.0.0"}, |
| "paths": { |
| "/health": {}, |
| "/metadata": {}, |
| "/schema": {}, |
| "/mcp": {}, |
| "/reset": {}, |
| "/step": {}, |
| "/state": {}, |
| }, |
| }, |
| ) |
| if url.endswith("/health"): |
| return _MockResponse(200, {"status": "healthy"}) |
| if url.endswith("/metadata"): |
| return _MockResponse(200, {"name": "EchoEnv", "description": "Echo env"}) |
| if url.endswith("/schema"): |
| return _MockResponse( |
| 200, |
| {"action": {"type": "object"}, "observation": {}, "state": {}}, |
| ) |
| raise AssertionError(f"Unexpected GET url: {url}") |
|
|
| def _fake_post(url: str, json: dict, timeout: float) -> _MockResponse: |
| if url.endswith("/mcp"): |
| return _MockResponse( |
| 200, |
| { |
| "jsonrpc": "2.0", |
| "id": None, |
| "error": {"code": -32600, "message": "Invalid Request"}, |
| }, |
| ) |
| raise AssertionError(f"Unexpected POST url: {url}") |
|
|
| with patch("openenv.cli._validation.requests.get", side_effect=_fake_get): |
| with patch("openenv.cli._validation.requests.post", side_effect=_fake_post): |
| report = validate_running_environment("http://localhost:8000") |
|
|
| assert report["passed"] is True |
| assert report["standard_version"] == "1.0.0" |
| assert report["mode"] == "simulation" |
| assert report["validation_type"] == "running_environment" |
| assert report["summary"]["passed_count"] == 6 |
| assert report["summary"]["total_count"] == 6 |
| assert report["summary"]["failed_criteria"] == [] |
|
|
|
|
| def test_validate_running_environment_failure() -> None: |
| """Runtime validator marks report as failed when criteria fail.""" |
|
|
| def _fake_get(url: str, timeout: float) -> _MockResponse: |
| if url.endswith("/openapi.json"): |
| return _MockResponse( |
| 200, |
| { |
| "info": {"version": "1.0.0"}, |
| "paths": { |
| "/health": {}, |
| "/metadata": {}, |
| "/schema": {}, |
| "/mcp": {}, |
| }, |
| }, |
| ) |
| if url.endswith("/health"): |
| return _MockResponse(200, {"status": "healthy"}) |
| if url.endswith("/metadata"): |
| return _MockResponse(500, {"detail": "boom"}) |
| if url.endswith("/schema"): |
| return _MockResponse( |
| 200, |
| {"action": {"type": "object"}, "observation": {}, "state": {}}, |
| ) |
| raise AssertionError(f"Unexpected GET url: {url}") |
|
|
| def _fake_post(url: str, json: dict, timeout: float) -> _MockResponse: |
| if url.endswith("/mcp"): |
| return _MockResponse( |
| 200, |
| { |
| "jsonrpc": "2.0", |
| "id": None, |
| "error": {"code": -32600, "message": "Invalid Request"}, |
| }, |
| ) |
| raise AssertionError(f"Unexpected POST url: {url}") |
|
|
| with patch("openenv.cli._validation.requests.get", side_effect=_fake_get): |
| with patch("openenv.cli._validation.requests.post", side_effect=_fake_post): |
| report = validate_running_environment("http://localhost:8000") |
|
|
| assert report["passed"] is False |
| metadata_checks = [c for c in report["criteria"] if c["id"] == "metadata_endpoint"] |
| assert metadata_checks |
| assert metadata_checks[0]["passed"] is False |
| assert report["summary"]["passed_count"] == 5 |
| assert report["summary"]["total_count"] == 6 |
| assert report["summary"]["failed_criteria"] == ["metadata_endpoint"] |
|
|
|
|
| def test_validate_command_runtime_target_outputs_json() -> None: |
| """CLI validates runtime targets and prints JSON report.""" |
| mock_report = { |
| "target": "https://example.com", |
| "validation_type": "running_environment", |
| "standard_version": "1.0.0", |
| "passed": True, |
| "criteria": [], |
| } |
|
|
| with patch( |
| "openenv.cli.commands.validate.validate_running_environment", |
| return_value=mock_report, |
| ) as mock_validate: |
| result = runner.invoke(app, ["validate", "https://example.com"]) |
|
|
| assert result.exit_code == 0 |
| assert json.loads(result.output) == mock_report |
| mock_validate.assert_called_once_with("https://example.com", timeout_s=5.0) |
|
|
|
|
| def test_validate_command_local_path_still_works(tmp_path: Path) -> None: |
| """CLI local validation remains backward compatible.""" |
| env_dir = tmp_path / "test_env" |
| _write_minimal_valid_env(env_dir) |
|
|
| result = runner.invoke(app, ["validate", str(env_dir)]) |
|
|
| assert result.exit_code == 0 |
| assert "[OK]" in result.output |
|
|
|
|
| def test_validate_command_local_json_output(tmp_path: Path) -> None: |
| """CLI can emit JSON report for local validation via --json.""" |
| env_dir = tmp_path / "test_env" |
| _write_minimal_valid_env(env_dir) |
|
|
| result = runner.invoke(app, ["validate", str(env_dir), "--json"]) |
|
|
| assert result.exit_code == 0 |
| payload = json.loads(result.output) |
| assert payload["validation_type"] == "local_environment" |
| assert payload["passed"] is True |
| assert payload["summary"]["passed_count"] == 1 |
| assert payload["summary"]["total_count"] == 1 |
| assert payload["summary"]["failed_criteria"] == [] |
|
|
|
|
| def test_validate_command_rejects_mixed_path_and_url(tmp_path: Path) -> None: |
| """CLI rejects mixing a local path argument with --url mode.""" |
| env_dir = tmp_path / "test_env" |
| _write_minimal_valid_env(env_dir) |
|
|
| result = runner.invoke( |
| app, |
| ["validate", str(env_dir), "--url", "http://localhost:8000"], |
| ) |
|
|
| assert result.exit_code != 0 |
| assert "Cannot combine a local path argument with --url" in result.output |
|
|