Skip to content

Commit 7ad9d7a

Browse files
committed
fix(updater): force git fetch on explicit refresh, bypassing the 5-min TTL
1 parent a498672 commit 7ad9d7a

3 files changed

Lines changed: 48 additions & 6 deletions

File tree

tests/updater/test_service_unit.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,44 @@ async def test_returns_all_components(self):
3131
assert "klipper" in result
3232
assert "system" in result
3333

34+
@pytest.mark.asyncio
35+
async def test_force_bypasses_ttl(self, tmp_path: Path):
36+
"""force=True must always fetch even when called within the TTL window."""
37+
fake_path = tmp_path / "klipper"
38+
fake_path.mkdir()
39+
component = ComponentConfig(name="klipper", kind="git", path=fake_path)
40+
fake = ComponentStatus(name="klipper", commits_behind=0)
41+
with (
42+
patch(
43+
"updater.service.load_components", return_value=([component], 3600.0)
44+
),
45+
patch("updater.service.check_git_status", return_value=fake) as mock_check,
46+
):
47+
svc = UpdateService()
48+
svc._fetch_times["klipper"] = time.monotonic()
49+
await svc.check_status(force=True)
50+
*_, skip_fetch = mock_check.call_args.args
51+
assert skip_fetch is False, "force=True must not skip the fetch"
52+
53+
@pytest.mark.asyncio
54+
async def test_ttl_suppresses_fetch_without_force(self, tmp_path: Path):
55+
"""Without force, a second call within the TTL window must skip the fetch."""
56+
fake_path = tmp_path / "klipper"
57+
fake_path.mkdir()
58+
component = ComponentConfig(name="klipper", kind="git", path=fake_path)
59+
fake = ComponentStatus(name="klipper", commits_behind=0)
60+
with (
61+
patch(
62+
"updater.service.load_components", return_value=([component], 3600.0)
63+
),
64+
patch("updater.service.check_git_status", return_value=fake) as mock_check,
65+
):
66+
svc = UpdateService()
67+
svc._fetch_times["klipper"] = time.monotonic()
68+
await svc.check_status()
69+
*_, skip_fetch = mock_check.call_args.args
70+
assert skip_fetch is True
71+
3472

3573
class TestStateFile:
3674
@pytest.mark.asyncio

updater/dbus_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,15 @@ def _validate_component_name(self, name: str) -> bool:
134134
self._invalid_requests = 0
135135
return is_valid
136136

137-
async def _emit_status(self) -> None:
137+
async def _emit_status(self, force: bool = False) -> None:
138138
"""Run check_status() and emit status_ready; emits error status per component on failure.""" # noqa: E501
139139
if self._status_check_in_progress:
140140
_log.debug("_emit_status: already in progress, queueing retry")
141141
self._status_pending = True
142142
return
143143
self._status_check_in_progress = True
144144
try:
145-
status = await self._svc.check_status()
145+
status = await self._svc.check_status(force=force)
146146
except Exception as exc: # noqa: BLE001
147147
_log.error("check_status failed: %s", exc)
148148
status = {
@@ -257,7 +257,7 @@ async def _run_recover(self, name: str, hard: bool) -> None: # noqa: FBT001
257257
async def request_status(self) -> None:
258258
"""D-Bus method: reply immediately, status arrives via status_ready signal."""
259259
_log.info("request_status called")
260-
self._spawn(self._emit_status(), name="request_status")
260+
self._spawn(self._emit_status(force=True), name="request_status")
261261

262262
@sdbus.dbus_method_async(result_signature="b")
263263
async def get_busy(self) -> bool:

updater/service.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ def has_component(self, name: str) -> bool:
8585
"""Return True if a component with the given name is registered."""
8686
return any(c.name == name for c in self._components)
8787

88-
async def check_status(self) -> dict[str, ComponentStatus]:
89-
"""Concurrently check status of all components."""
88+
async def check_status(self, force: bool = False) -> dict[str, ComponentStatus]:
89+
"""Concurrently check status of all components.
90+
91+
force=True bypasses the fetch TTL so git fetch always runs.
92+
Use for explicit user-triggered refreshes.
93+
"""
9094
results: dict[str, ComponentStatus] = {}
9195

9296
async def _check_one(c: ComponentConfig) -> None:
@@ -98,7 +102,7 @@ async def _check_one(c: ComponentConfig) -> None:
98102
now = time.monotonic()
99103
async with self._git_lock:
100104
last = self._fetch_times.get(c.name, 0.0)
101-
skip_fetch = (now - last) < self._FETCH_TTL
105+
skip_fetch = not force and (now - last) < self._FETCH_TTL
102106
if not skip_fetch:
103107
self._fetch_times[c.name] = now
104108
status = await check_git_status(

0 commit comments

Comments
 (0)