refactor: DRY PRESENCE_FILE — single source of truth in workshop_state (#381)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user