diff --git a/packages/office/CHANGELOG.md b/packages/office/CHANGELOG.md index d3992ac..8793445 100644 --- a/packages/office/CHANGELOG.md +++ b/packages/office/CHANGELOG.md @@ -61,6 +61,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 OOM-immune processes (`oom_score_adj <= -900`) and hides on hosts without `/proc`. +- Hub test coverage for the ecosystem proxy (DL-17a7): `test_hub.py` + gains an integration case that stands up a real upstream HTTP server + serving an eco payload, points `OFFICE_ECO_URL` at it, and asserts + `GET /api/eco` returns that payload under the `"eco"` key next to the + office state — locking in the chain the `#eco` side panel depends on + (office daemon `/api/eco-raw` → rtunnel → `OFFICE_ECO_URL` → hub + `/api/eco`). A second case asserts the unreachable-upstream fallback + (`{"error": "ecosystem API unreachable"}`). + ### Fixed - `session_locked()` on Linux never detected open sessions: POSIX locks diff --git a/packages/office/tests/test_hub.py b/packages/office/tests/test_hub.py index b80cc78..929634f 100644 --- a/packages/office/tests/test_hub.py +++ b/packages/office/tests/test_hub.py @@ -3,12 +3,31 @@ import unittest import urllib.error import urllib.request -from http.server import ThreadingHTTPServer +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from unittest.mock import patch from devin_office import hub +def _eco_upstream(payload: dict) -> ThreadingHTTPServer: + """HTTP server that serves a fixed eco payload — stands in for the + office daemon's /api/eco-raw behind OFFICE_ECO_URL.""" + body = json.dumps(payload).encode() + + class _Handler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, *args): + pass + + return ThreadingHTTPServer(("127.0.0.1", 0), _Handler) + + class HubSecurityTests(unittest.TestCase): def test_loopback_can_run_without_a_token(self): hub.validate_binding("127.0.0.1", "") @@ -55,6 +74,59 @@ def test_ecosystem_proxy_is_disabled_by_default(self): self.assertIsNone(result["eco"]) self.assertEqual(result["office"], {"agents": []}) + def test_ecosystem_proxy_returns_upstream_data(self): + """DL-17a7: with OFFICE_ECO_URL set, GET /api/eco must proxy the + upstream eco payload so the side panel renders real data.""" + upstream_payload = { + "office-laptop": { + "probes": {"mcp-hub": 200, "vm-office": 200}, + "tools": {"unified-mcp": True, "slack-poll": False}, + "units": {"devin-janitor.timer": "active"}, + "meta": {"repos": 10, "clis": 42}, + } + } + upstream = _eco_upstream(upstream_payload) + upstream_thread = threading.Thread( + target=upstream.serve_forever, daemon=True) + upstream_thread.start() + server = ThreadingHTTPServer(("127.0.0.1", 0), hub.Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + eco_url = f"http://127.0.0.1:{upstream.server_port}/api/eco-raw" + try: + with patch.object(hub, "ECO_URL", eco_url), patch.object( + hub, "TOKEN", "" + ), patch.object( + hub, "_eco_cache", {"at": 0.0, "data": None} + ), patch.object( + hub, "current_state", return_value={"agents": []} + ): + thread.start() + base = f"http://127.0.0.1:{server.server_port}" + with urllib.request.urlopen(base + "/api/eco") as resp: + data = json.loads(resp.read().decode()) + self.assertEqual(data["eco"], upstream_payload) + self.assertEqual(data["office"], {"agents": []}) + finally: + server.shutdown() + server.server_close() + thread.join() + upstream.shutdown() + upstream.server_close() + upstream_thread.join() + + def test_ecosystem_proxy_reports_unreachable_upstream(self): + with patch.object(hub, "ECO_URL", "http://127.0.0.1:9/eco"), patch.object( + hub, "_eco_cache", {"at": 0.0, "data": None} + ), patch.object( + hub, "current_state", return_value={"agents": []} + ), patch( + "urllib.request.urlopen", side_effect=OSError("refused") + ): + result = hub.eco_status() + self.assertEqual(result["eco"], + {"error": "ecosystem API unreachable"}) + self.assertEqual(result["office"], {"agents": []}) + if __name__ == "__main__": unittest.main()