From 19e7e61c92b275a8d252aafed8ed5f172883e14e Mon Sep 17 00:00:00 2001 From: hermes Date: Wed, 18 Mar 2026 22:33:06 -0400 Subject: [PATCH] =?UTF-8?q?[loop-cycle]=20refactor:=20DRY=20PRESENCE=5FFIL?= =?UTF-8?q?E=20=E2=80=94=20single=20source=20of=20truth=20in=20workshop=5F?= =?UTF-8?q?state=20(#381)=20(#382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dashboard/app.py | 8 ++++---- src/dashboard/routes/world.py | 11 +++++------ tests/dashboard/test_world_api.py | 14 +++++++------- tests/integrations/test_presence_watcher.py | 2 +- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/dashboard/app.py b/src/dashboard/app.py index 0d153073..03c99c77 100644 --- a/src/dashboard/app.py +++ b/src/dashboard/app.py @@ -49,6 +49,7 @@ from dashboard.routes.tools import router as tools_router from dashboard.routes.voice import router as voice_router from dashboard.routes.work_orders import router as work_orders_router from dashboard.routes.world import router as world_router +from timmy.workshop_state import PRESENCE_FILE class _ColorFormatter(logging.Formatter): @@ -190,7 +191,6 @@ async def _loop_qa_scheduler() -> None: await asyncio.sleep(interval) -_PRESENCE_FILE = Path.home() / ".timmy" / "presence.json" _PRESENCE_POLL_SECONDS = 30 _PRESENCE_INITIAL_DELAY = 3 @@ -219,11 +219,11 @@ async def _presence_watcher() -> None: while True: try: - if _PRESENCE_FILE.exists(): - mtime = _PRESENCE_FILE.stat().st_mtime + if PRESENCE_FILE.exists(): + mtime = PRESENCE_FILE.stat().st_mtime if mtime != last_mtime: last_mtime = mtime - raw = await asyncio.to_thread(_PRESENCE_FILE.read_text) + raw = await asyncio.to_thread(PRESENCE_FILE.read_text) state = json.loads(raw) await ws_mgr.broadcast("timmy_state", state) else: diff --git a/src/dashboard/routes/world.py b/src/dashboard/routes/world.py index 61623b42..91af8b0d 100644 --- a/src/dashboard/routes/world.py +++ b/src/dashboard/routes/world.py @@ -14,29 +14,28 @@ import json import logging import time from datetime import UTC, datetime -from pathlib import Path from fastapi import APIRouter from fastapi.responses import JSONResponse +from timmy.workshop_state import PRESENCE_FILE + logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/world", tags=["world"]) - -_PRESENCE_FILE = Path.home() / ".timmy" / "presence.json" _STALE_THRESHOLD = 90 # seconds — file older than this triggers live rebuild def _read_presence_file() -> dict | None: """Read presence.json if it exists and is fresh enough.""" try: - if not _PRESENCE_FILE.exists(): + if not PRESENCE_FILE.exists(): return None - age = time.time() - _PRESENCE_FILE.stat().st_mtime + age = time.time() - PRESENCE_FILE.stat().st_mtime if age > _STALE_THRESHOLD: logger.debug("presence.json is stale (%.0fs old)", age) return None - return json.loads(_PRESENCE_FILE.read_text()) + return json.loads(PRESENCE_FILE.read_text()) except (OSError, json.JSONDecodeError) as exc: logger.warning("Failed to read presence.json: %s", exc) return None diff --git a/tests/dashboard/test_world_api.py b/tests/dashboard/test_world_api.py index ada37788..d18d6377 100644 --- a/tests/dashboard/test_world_api.py +++ b/tests/dashboard/test_world_api.py @@ -55,7 +55,7 @@ def test_build_world_state_defaults(): def test_read_presence_file_missing(tmp_path): - with patch("dashboard.routes.world._PRESENCE_FILE", tmp_path / "nope.json"): + with patch("dashboard.routes.world.PRESENCE_FILE", tmp_path / "nope.json"): assert _read_presence_file() is None @@ -67,14 +67,14 @@ def test_read_presence_file_stale(tmp_path): import os os.utime(f, (stale_time, stale_time)) - with patch("dashboard.routes.world._PRESENCE_FILE", f): + with patch("dashboard.routes.world.PRESENCE_FILE", f): assert _read_presence_file() is None def test_read_presence_file_fresh(tmp_path): f = tmp_path / "presence.json" f.write_text(json.dumps({"version": 1, "mood": "focused"})) - with patch("dashboard.routes.world._PRESENCE_FILE", f): + with patch("dashboard.routes.world.PRESENCE_FILE", f): result = _read_presence_file() assert result is not None assert result["version"] == 1 @@ -83,7 +83,7 @@ def test_read_presence_file_fresh(tmp_path): def test_read_presence_file_bad_json(tmp_path): f = tmp_path / "presence.json" f.write_text("not json {{{") - with patch("dashboard.routes.world._PRESENCE_FILE", f): + with patch("dashboard.routes.world.PRESENCE_FILE", f): assert _read_presence_file() is None @@ -120,7 +120,7 @@ def test_world_state_endpoint_with_file(client, tmp_path): } ) ) - with patch("dashboard.routes.world._PRESENCE_FILE", f): + with patch("dashboard.routes.world.PRESENCE_FILE", f): resp = client.get("/api/world/state") assert resp.status_code == 200 @@ -133,7 +133,7 @@ def test_world_state_endpoint_with_file(client, tmp_path): def test_world_state_endpoint_fallback(client, tmp_path): """Endpoint falls back to live state when file missing.""" with ( - patch("dashboard.routes.world._PRESENCE_FILE", tmp_path / "nope.json"), + patch("dashboard.routes.world.PRESENCE_FILE", tmp_path / "nope.json"), patch("timmy.workshop_state.get_state_dict") as mock_get, ): mock_get.return_value = { @@ -154,7 +154,7 @@ def test_world_state_endpoint_fallback(client, tmp_path): def test_world_state_endpoint_full_fallback(client, tmp_path): """Endpoint returns safe defaults when everything fails.""" with ( - patch("dashboard.routes.world._PRESENCE_FILE", tmp_path / "nope.json"), + patch("dashboard.routes.world.PRESENCE_FILE", tmp_path / "nope.json"), patch( "timmy.workshop_state.get_state_dict", side_effect=RuntimeError("boom"), diff --git a/tests/integrations/test_presence_watcher.py b/tests/integrations/test_presence_watcher.py index 99db2202..b728026b 100644 --- a/tests/integrations/test_presence_watcher.py +++ b/tests/integrations/test_presence_watcher.py @@ -18,7 +18,7 @@ def _patches(mock_ws, presence_file): from contextlib import ExitStack stack = ExitStack() - stack.enter_context(patch("dashboard.app._PRESENCE_FILE", presence_file)) + stack.enter_context(patch("dashboard.app.PRESENCE_FILE", presence_file)) stack.enter_context(patch("infrastructure.ws_manager.handler.ws_manager", mock_ws)) for key, val in _FAST.items(): stack.enter_context(patch(key, val))