"""Tests for the observation and context-building layer (context.py). All tests are offline — httpx is mocked so no live state-hub required. """ from __future__ import annotations from pathlib import Path from unittest.mock import MagicMock, patch import pytest from context import ( build_context, fetch_state, load_constitution, CONSTITUTION_PATH, API_BASE, ) # --------------------------------------------------------------------------- # fetch_state # --------------------------------------------------------------------------- class TestFetchState: def _mock_response(self, data: dict, status: int = 200): resp = MagicMock() resp.status_code = status resp.json.return_value = data resp.raise_for_status = MagicMock() return resp def test_fetch_state_calls_summary_endpoint(self): state_data = {"totals": {"tasks": {"todo": 5}}, "topics": []} with patch("httpx.get") as mock_get: mock_get.return_value = self._mock_response(state_data) result = fetch_state(domain=None) mock_get.assert_called_once() call_url = mock_get.call_args[0][0] assert "/state/summary" in call_url def test_fetch_state_with_domain_calls_domain_endpoint(self): domain_data = {"domain": "custodian", "workstreams": []} with patch("httpx.get") as mock_get: mock_get.return_value = self._mock_response(domain_data) result = fetch_state(domain="custodian") call_url = mock_get.call_args[0][0] assert "custodian" in call_url def test_fetch_state_returns_dict(self): state_data = {"totals": {}, "topics": []} with patch("httpx.get") as mock_get: mock_get.return_value = self._mock_response(state_data) result = fetch_state() assert isinstance(result, dict) def test_fetch_state_handles_connection_error(self): with patch("httpx.get") as mock_get: mock_get.side_effect = Exception("Connection refused") result = fetch_state() # Graceful degradation: returns empty dict, does not raise assert result == {} def test_fetch_state_handles_non_200(self): resp = MagicMock() resp.raise_for_status.side_effect = Exception("503 Service Unavailable") with patch("httpx.get") as mock_get: mock_get.return_value = resp result = fetch_state() assert result == {} def test_fetch_state_custom_api_base(self): with patch("httpx.get") as mock_get: mock_get.return_value = self._mock_response({}) fetch_state(api_base="http://localhost:9999") call_url = mock_get.call_args[0][0] assert "localhost:9999" in call_url # --------------------------------------------------------------------------- # load_constitution # --------------------------------------------------------------------------- class TestLoadConstitution: def test_load_constitution_returns_non_empty_string(self): text = load_constitution() assert isinstance(text, str) assert len(text) > 100 def test_load_constitution_contains_key_clauses(self): text = load_constitution() assert "Custodian" in text assert "Forbidden" in text or "forbidden" in text.lower() def test_constitution_path_exists(self): assert CONSTITUTION_PATH.exists(), ( f"Constitution not found at {CONSTITUTION_PATH}. " "The path is hardcoded relative to this file — check context.py." ) def test_load_constitution_with_missing_file(self, tmp_path, monkeypatch): """If constitution is missing, return a minimal fallback, not an exception.""" import context as ctx_module monkeypatch.setattr(ctx_module, "CONSTITUTION_PATH", tmp_path / "nonexistent.md") text = load_constitution() assert isinstance(text, str) # Should return fallback, not crash assert len(text) > 0 # --------------------------------------------------------------------------- # build_context # --------------------------------------------------------------------------- class TestBuildContext: def _minimal_state(self) -> dict: return { "totals": { "tasks": {"todo": 3, "done": 10}, "workstreams": {"active": 2, "completed": 5}, "decisions": {"open": 0}, }, "blocking_decisions": [], "blocked_tasks": [], "open_workstreams": [], } def test_build_context_returns_string(self): ctx = build_context(self._minimal_state(), "## Constitution\nBe safe.") assert isinstance(ctx, str) def test_build_context_includes_constitution(self): ctx = build_context(self._minimal_state(), "## Constitution\nBe safe.") assert "Be safe" in ctx def test_build_context_includes_task_counts(self): ctx = build_context(self._minimal_state(), "") assert "3" in ctx # todo count def test_build_context_mentions_blocked_tasks_when_present(self): state = self._minimal_state() state["blocked_tasks"] = [ {"id": "t1", "title": "Deploy postgres", "blocking_reason": "no cluster"} ] ctx = build_context(state, "") assert "Deploy postgres" in ctx or "blocked" in ctx.lower() def test_build_context_mentions_blocking_decisions_when_present(self): state = self._minimal_state() state["blocking_decisions"] = [ {"id": "d1", "title": "Which DB?", "type": "pending"} ] ctx = build_context(state, "") assert "Which DB?" in ctx or "decision" in ctx.lower() def test_build_context_with_empty_state_does_not_crash(self): ctx = build_context({}, "some constitution") assert isinstance(ctx, str) def test_build_context_includes_json_response_instruction(self): """The prompt must instruct the LLM to return a JSON block.""" ctx = build_context(self._minimal_state(), "") assert "```json" in ctx or "JSON" in ctx