fix(settings): the plugin catalog is fetched by a scheduled task, never by a page load
The operator's ruling (Background Jobs, 2026-10-08): a Steward page reads stored records; the sources are contacted by scheduled collection. An audit of every GET handler, HTMX fragment, widget poll and MCP read tool found one page load that reached out: Settings -> Plugins' catalog fragment fetched each plugin repository (5 min cache, 10 s timeout each, retried on every load when unreachable). plugin_catalog_refresh ticks every minute and fetches a repository whose copy is missing or older than an hour, waiting 5 min after a failure; saving repositories clears the cache, so the next tick fetches. The fragment only reads the cache and says which repositories are not fetched yet. Refresh is now a POST that fetches at once, as does install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,7 +101,7 @@ async def test_route_takes_url_and_checksum_from_the_catalog(client):
|
||||
checksum_sha256="abc")
|
||||
install = AsyncMock(return_value=(False, "stopped here"))
|
||||
with patch("steward.settings.routes.get_all_settings", new=AsyncMock(return_value={})), \
|
||||
patch("steward.settings.routes._get_plugin_repos", return_value=[{"url": "https://catalog/"}]), \
|
||||
patch("steward.settings.routes.repo_urls", return_value=["https://catalog/"]), \
|
||||
patch("steward.core.plugin_index.fetch_catalog", new=AsyncMock(return_value=[entry])), \
|
||||
patch("steward.core.plugin_manager.download_and_install_plugin", new=install), \
|
||||
patch("steward.settings.routes.log_audit", new=AsyncMock()):
|
||||
@@ -114,3 +114,64 @@ async def test_route_takes_url_and_checksum_from_the_catalog(client):
|
||||
resp = await client.post("/settings/plugins/install/other", headers={"X-CSRF-Token": "t"})
|
||||
assert "No configured plugin repository lists this plugin" in await resp.get_data(as_text=True)
|
||||
install.assert_not_awaited()
|
||||
|
||||
|
||||
# ── The catalog is fetched by a scheduled task, never by a page load ─────────
|
||||
# Background Jobs ruling (operator, 2026-10-08): a Steward page reads stored
|
||||
# records; the sources are contacted by scheduled collection, or because the
|
||||
# operator asked (Refresh, install).
|
||||
|
||||
from steward.core import plugin_index as pi # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fresh_catalog():
|
||||
pi.clear_catalog_cache()
|
||||
yield
|
||||
pi.clear_catalog_cache()
|
||||
|
||||
|
||||
def _fetcher(fetched, fail=()):
|
||||
async def fetch(url):
|
||||
fetched.append(url)
|
||||
if url in fail:
|
||||
pi._failed_at[url] = 0.0
|
||||
return False
|
||||
pi._url_cache[url] = ({"plugins": [{"name": f"p-{url[-1]}", "version": "1.0"}]}, 0.0)
|
||||
return True
|
||||
return fetch
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_tick_fetches_what_is_due_and_waits_out_a_failure(fresh_catalog):
|
||||
fetched = []
|
||||
with patch.object(pi, "_fetch_one", new=_fetcher(fetched, fail={"https://r/b"})):
|
||||
assert await pi.refresh_due(["https://r/a", "https://r/b"], now=10.0) == 1
|
||||
assert fetched == ["https://r/a", "https://r/b"]
|
||||
fetched.clear()
|
||||
# a fresh copy is left alone; the failure is not retried before RETRY_AFTER
|
||||
assert await pi.refresh_due(["https://r/a", "https://r/b"], now=pi.RETRY_AFTER - 1.0) == 0
|
||||
assert fetched == []
|
||||
assert await pi.refresh_due(["https://r/a", "https://r/b"], now=pi.RETRY_AFTER + 1.0) == 0
|
||||
assert fetched == ["https://r/b"]
|
||||
fetched.clear()
|
||||
assert await pi.refresh_due(["https://r/a"], now=pi.CATALOG_MAX_AGE + 1.0) == 1
|
||||
assert fetched == ["https://r/a"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_catalog_page_reads_the_cache_and_only_refresh_fetches(client, fresh_catalog):
|
||||
async with client.session_transaction() as sess:
|
||||
sess.update(user_id="u", user_role="admin", username="admin", _csrf="t")
|
||||
fetched = []
|
||||
with patch("steward.settings.routes.get_all_settings", new=AsyncMock(return_value={})), \
|
||||
patch("steward.settings.routes.repo_urls", return_value=["https://r/a"]), \
|
||||
patch.object(pi, "_fetch_one", new=_fetcher(fetched)):
|
||||
page = await (await client.get("/settings/plugins/catalog/")).get_data(as_text=True)
|
||||
assert fetched == [] and "Not fetched yet: https://r/a" in page
|
||||
page = await (await client.post("/settings/plugins/catalog/refresh",
|
||||
headers={"X-CSRF-Token": "t"})).get_data(as_text=True)
|
||||
assert fetched == ["https://r/a"] and "p-a" in page and "Not fetched yet" not in page
|
||||
# the next page load shows the stored copy without fetching again
|
||||
page = await (await client.get("/settings/plugins/catalog/")).get_data(as_text=True)
|
||||
assert fetched == ["https://r/a"] and "p-a" in page
|
||||
|
||||
Reference in New Issue
Block a user