refactor: DRY PRESENCE_FILE — single source of truth in workshop_state (#381)
All checks were successful
Tests / lint (pull_request) Successful in 3s
Tests / test (pull_request) Successful in 1m25s

This commit is contained in:
2026-03-18 22:28:46 -04:00
parent b7573432cc
commit 51c27c6974
4 changed files with 17 additions and 18 deletions

View File

@@ -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.voice import router as voice_router
from dashboard.routes.work_orders import router as work_orders_router from dashboard.routes.work_orders import router as work_orders_router
from dashboard.routes.world import router as world_router from dashboard.routes.world import router as world_router
from timmy.workshop_state import PRESENCE_FILE
class _ColorFormatter(logging.Formatter): class _ColorFormatter(logging.Formatter):
@@ -190,7 +191,6 @@ async def _loop_qa_scheduler() -> None:
await asyncio.sleep(interval) await asyncio.sleep(interval)
_PRESENCE_FILE = Path.home() / ".timmy" / "presence.json"
_PRESENCE_POLL_SECONDS = 30 _PRESENCE_POLL_SECONDS = 30
_PRESENCE_INITIAL_DELAY = 3 _PRESENCE_INITIAL_DELAY = 3
@@ -219,11 +219,11 @@ async def _presence_watcher() -> None:
while True: while True:
try: try:
if _PRESENCE_FILE.exists(): if PRESENCE_FILE.exists():
mtime = _PRESENCE_FILE.stat().st_mtime mtime = PRESENCE_FILE.stat().st_mtime
if mtime != last_mtime: if mtime != last_mtime:
last_mtime = 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) state = json.loads(raw)
await ws_mgr.broadcast("timmy_state", state) await ws_mgr.broadcast("timmy_state", state)
else: else:

View File

@@ -14,29 +14,28 @@ import json
import logging import logging
import time import time
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from timmy.workshop_state import PRESENCE_FILE
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/world", tags=["world"]) 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 _STALE_THRESHOLD = 90 # seconds — file older than this triggers live rebuild
def _read_presence_file() -> dict | None: def _read_presence_file() -> dict | None:
"""Read presence.json if it exists and is fresh enough.""" """Read presence.json if it exists and is fresh enough."""
try: try:
if not _PRESENCE_FILE.exists(): if not PRESENCE_FILE.exists():
return None return None
age = time.time() - _PRESENCE_FILE.stat().st_mtime age = time.time() - PRESENCE_FILE.stat().st_mtime
if age > _STALE_THRESHOLD: if age > _STALE_THRESHOLD:
logger.debug("presence.json is stale (%.0fs old)", age) logger.debug("presence.json is stale (%.0fs old)", age)
return None return None
return json.loads(_PRESENCE_FILE.read_text()) return json.loads(PRESENCE_FILE.read_text())
except (OSError, json.JSONDecodeError) as exc: except (OSError, json.JSONDecodeError) as exc:
logger.warning("Failed to read presence.json: %s", exc) logger.warning("Failed to read presence.json: %s", exc)
return None return None

View File

@@ -55,7 +55,7 @@ def test_build_world_state_defaults():
def test_read_presence_file_missing(tmp_path): 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 assert _read_presence_file() is None
@@ -67,14 +67,14 @@ def test_read_presence_file_stale(tmp_path):
import os import os
os.utime(f, (stale_time, stale_time)) 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 assert _read_presence_file() is None
def test_read_presence_file_fresh(tmp_path): def test_read_presence_file_fresh(tmp_path):
f = tmp_path / "presence.json" f = tmp_path / "presence.json"
f.write_text(json.dumps({"version": 1, "mood": "focused"})) 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() result = _read_presence_file()
assert result is not None assert result is not None
assert result["version"] == 1 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): def test_read_presence_file_bad_json(tmp_path):
f = tmp_path / "presence.json" f = tmp_path / "presence.json"
f.write_text("not 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 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") resp = client.get("/api/world/state")
assert resp.status_code == 200 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): def test_world_state_endpoint_fallback(client, tmp_path):
"""Endpoint falls back to live state when file missing.""" """Endpoint falls back to live state when file missing."""
with ( 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, patch("timmy.workshop_state.get_state_dict") as mock_get,
): ):
mock_get.return_value = { 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): def test_world_state_endpoint_full_fallback(client, tmp_path):
"""Endpoint returns safe defaults when everything fails.""" """Endpoint returns safe defaults when everything fails."""
with ( with (
patch("dashboard.routes.world._PRESENCE_FILE", tmp_path / "nope.json"), patch("dashboard.routes.world.PRESENCE_FILE", tmp_path / "nope.json"),
patch( patch(
"timmy.workshop_state.get_state_dict", "timmy.workshop_state.get_state_dict",
side_effect=RuntimeError("boom"), side_effect=RuntimeError("boom"),

View File

@@ -18,7 +18,7 @@ def _patches(mock_ws, presence_file):
from contextlib import ExitStack from contextlib import ExitStack
stack = 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)) stack.enter_context(patch("infrastructure.ws_manager.handler.ws_manager", mock_ws))
for key, val in _FAST.items(): for key, val in _FAST.items():
stack.enter_context(patch(key, val)) stack.enter_context(patch(key, val))