fix: refresh optional CLI Apps catalogs
maintainer edit: CLI Apps settings now treats optional catalog caches as refresh candidates without blocking the initial payload, and pending polling stops when refresh is throttled instead of running indefinitely.
This commit is contained in:
@@ -510,9 +510,9 @@ class CliAppManager:
|
||||
apps_by_name[key] = entry
|
||||
return list(apps_by_name.values()), max(updated_values) if updated_values else None
|
||||
|
||||
def catalog_cache_fresh(self) -> bool:
|
||||
def catalog_cache_fresh(self, *, include_optional: bool = False) -> bool:
|
||||
for source, _url, _raw_base, required in _CATALOG_SOURCES:
|
||||
if not required:
|
||||
if not required and not include_optional:
|
||||
continue
|
||||
data, cached_at = self._cached_registry(self._cache_path(source))
|
||||
if data is None or _now() - cached_at >= self.runtime.catalog_ttl_seconds:
|
||||
|
||||
@@ -34,7 +34,7 @@ def _start_catalog_refresh() -> bool:
|
||||
if _catalog_refresh_running:
|
||||
return True
|
||||
if now - _catalog_refresh_last_started < _CATALOG_REFRESH_RETRY_SECONDS:
|
||||
return True
|
||||
return False
|
||||
_catalog_refresh_running = True
|
||||
_catalog_refresh_last_started = now
|
||||
|
||||
@@ -113,9 +113,9 @@ def cli_apps_payload(*, installed_only: bool = False) -> dict[str, Any]:
|
||||
if installed_only:
|
||||
return manager.installed_payload()
|
||||
payload = manager.payload(cache_only=True)
|
||||
refresh_pending = not manager.catalog_cache_fresh()
|
||||
if refresh_pending:
|
||||
_start_catalog_refresh()
|
||||
refresh_pending = False
|
||||
if not manager.catalog_cache_fresh(include_optional=True):
|
||||
refresh_pending = _start_catalog_refresh()
|
||||
if not payload["apps"]:
|
||||
installed = manager.installed_payload()
|
||||
if installed["apps"]:
|
||||
|
||||
@@ -286,6 +286,21 @@ def test_payload_cache_only_does_not_fetch_catalog(tmp_path: Path, monkeypatch:
|
||||
assert manager.catalog_cache_fresh() is True
|
||||
|
||||
|
||||
def test_catalog_cache_fresh_can_include_optional_sources(tmp_path: Path) -> None:
|
||||
manager = _manager(tmp_path)
|
||||
_write_cache(
|
||||
manager._cache_path("harness"),
|
||||
{"meta": {"updated": "2026-04-16"}, "clis": []},
|
||||
)
|
||||
_write_cache(
|
||||
manager._cache_path("public"),
|
||||
{"meta": {"updated": "2026-04-18"}, "clis": []},
|
||||
)
|
||||
|
||||
assert manager.catalog_cache_fresh() is True
|
||||
assert manager.catalog_cache_fresh(include_optional=True) is False
|
||||
|
||||
|
||||
def test_payload_cache_only_without_cache_returns_empty(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
manager = _manager(tmp_path)
|
||||
|
||||
|
||||
@@ -6,10 +6,18 @@ from nanobot.webui import cli_apps_api
|
||||
|
||||
|
||||
class _FakeManager:
|
||||
def __init__(self, *, fresh: bool, apps: list[dict[str, Any]] | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
fresh: bool,
|
||||
apps: list[dict[str, Any]] | None = None,
|
||||
all_sources_fresh: bool | None = None,
|
||||
) -> None:
|
||||
self.fresh = fresh
|
||||
self.all_sources_fresh = fresh if all_sources_fresh is None else all_sources_fresh
|
||||
self.apps = apps or []
|
||||
self.payload_calls: list[bool] = []
|
||||
self.fresh_checks: list[bool] = []
|
||||
|
||||
def payload(self, *, cache_only: bool = False) -> dict[str, Any]:
|
||||
self.payload_calls.append(cache_only)
|
||||
@@ -19,8 +27,9 @@ class _FakeManager:
|
||||
"catalog_updated_at": "2026-04-18" if self.apps else None,
|
||||
}
|
||||
|
||||
def catalog_cache_fresh(self) -> bool:
|
||||
return self.fresh
|
||||
def catalog_cache_fresh(self, *, include_optional: bool = False) -> bool:
|
||||
self.fresh_checks.append(include_optional)
|
||||
return self.all_sources_fresh if include_optional else self.fresh
|
||||
|
||||
def installed_payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
@@ -51,11 +60,12 @@ def test_cli_apps_payload_uses_cache_and_marks_refresh_pending(monkeypatch) -> N
|
||||
manager = _FakeManager(fresh=False)
|
||||
refreshes = []
|
||||
monkeypatch.setattr(cli_apps_api, "_manager", lambda: manager)
|
||||
monkeypatch.setattr(cli_apps_api, "_start_catalog_refresh", lambda: refreshes.append(True))
|
||||
monkeypatch.setattr(cli_apps_api, "_start_catalog_refresh", lambda: refreshes.append(True) or True)
|
||||
|
||||
payload = cli_apps_api.cli_apps_payload()
|
||||
|
||||
assert manager.payload_calls == [True]
|
||||
assert manager.fresh_checks == [True]
|
||||
assert refreshes == [True]
|
||||
assert payload["catalog_refresh_pending"] is True
|
||||
assert payload["apps"][0]["name"] == "gimp"
|
||||
@@ -85,11 +95,61 @@ def test_cli_apps_payload_skips_refresh_when_cache_is_fresh(monkeypatch) -> None
|
||||
)
|
||||
refreshes = []
|
||||
monkeypatch.setattr(cli_apps_api, "_manager", lambda: manager)
|
||||
monkeypatch.setattr(cli_apps_api, "_start_catalog_refresh", lambda: refreshes.append(True))
|
||||
monkeypatch.setattr(cli_apps_api, "_start_catalog_refresh", lambda: refreshes.append(True) or True)
|
||||
|
||||
payload = cli_apps_api.cli_apps_payload()
|
||||
|
||||
assert manager.payload_calls == [True]
|
||||
assert manager.fresh_checks == [True]
|
||||
assert refreshes == []
|
||||
assert payload["catalog_refresh_pending"] is False
|
||||
assert payload["apps"][0]["source"] == "harness"
|
||||
|
||||
|
||||
def test_cli_apps_payload_refreshes_when_optional_cache_is_stale(monkeypatch) -> None:
|
||||
manager = _FakeManager(
|
||||
fresh=True,
|
||||
all_sources_fresh=False,
|
||||
apps=[
|
||||
{
|
||||
"name": "gimp",
|
||||
"display_name": "GIMP",
|
||||
"category": "image",
|
||||
"description": "Image editing",
|
||||
"requires": "Python",
|
||||
"source": "harness",
|
||||
"entry_point": "cli-anything-gimp",
|
||||
"install_supported": True,
|
||||
"installed": False,
|
||||
"available": False,
|
||||
"status": "not_installed",
|
||||
"logo_url": None,
|
||||
"brand_color": None,
|
||||
"skill_installed": False,
|
||||
}
|
||||
],
|
||||
)
|
||||
refreshes = []
|
||||
monkeypatch.setattr(cli_apps_api, "_manager", lambda: manager)
|
||||
monkeypatch.setattr(cli_apps_api, "_start_catalog_refresh", lambda: refreshes.append(True) or True)
|
||||
|
||||
payload = cli_apps_api.cli_apps_payload()
|
||||
|
||||
assert manager.payload_calls == [True]
|
||||
assert manager.fresh_checks == [True]
|
||||
assert refreshes == [True]
|
||||
assert payload["catalog_refresh_pending"] is True
|
||||
assert payload["apps"][0]["source"] == "harness"
|
||||
|
||||
|
||||
def test_cli_apps_payload_reports_not_pending_when_refresh_is_throttled(monkeypatch) -> None:
|
||||
manager = _FakeManager(fresh=False)
|
||||
monkeypatch.setattr(cli_apps_api, "_manager", lambda: manager)
|
||||
monkeypatch.setattr(cli_apps_api, "_start_catalog_refresh", lambda: False)
|
||||
|
||||
payload = cli_apps_api.cli_apps_payload()
|
||||
|
||||
assert manager.payload_calls == [True]
|
||||
assert manager.fresh_checks == [True]
|
||||
assert payload["catalog_refresh_pending"] is False
|
||||
assert payload["apps"][0]["name"] == "gimp"
|
||||
|
||||
@@ -213,6 +213,8 @@ const DEFERRED_MODEL_LIST_PROVIDERS = new Set([
|
||||
"volcengine_coding_plan",
|
||||
]);
|
||||
const DEFERRED_MODEL_LIST_QUERY_MIN_LENGTH = 2;
|
||||
const CLI_APPS_REFRESH_RETRY_MS = 2_000;
|
||||
const CLI_APPS_REFRESH_MAX_RETRIES = 30;
|
||||
|
||||
const FALLBACK_TIMEZONES = [
|
||||
"UTC",
|
||||
@@ -696,13 +698,18 @@ export function SettingsView({
|
||||
if (activeSection !== "apps") return;
|
||||
let cancelled = false;
|
||||
let retry: number | null = null;
|
||||
let retryCount = 0;
|
||||
const loadCliApps = (showLoading: boolean) => {
|
||||
if (showLoading) setCliAppsLoading(true);
|
||||
fetchCliApps(token)
|
||||
.then((payload) => {
|
||||
if (cancelled) return;
|
||||
if (payload.catalog_refresh_pending) {
|
||||
retry = window.setTimeout(() => loadCliApps(false), 2000);
|
||||
if (payload.catalog_refresh_pending && retryCount < CLI_APPS_REFRESH_MAX_RETRIES) {
|
||||
retryCount += 1;
|
||||
retry = window.setTimeout(() => {
|
||||
retry = null;
|
||||
loadCliApps(false);
|
||||
}, CLI_APPS_REFRESH_RETRY_MS);
|
||||
}
|
||||
setCliApps(payload);
|
||||
setCliAppsError(null);
|
||||
|
||||
Reference in New Issue
Block a user