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.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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user