fc3d: settings API exposes scheduling knobs with bounds validation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 16:04:19 -04:00
parent 69b3fa2ba9
commit 28a4ed3d38
2 changed files with 87 additions and 0 deletions
+25
View File
@@ -22,6 +22,9 @@ _EDITABLE_FIELDS = (
"phash_threshold",
"download_rate_limit_seconds",
"download_validate_files",
"download_schedule_default_seconds",
"download_event_retention_days",
"download_failure_warning_threshold",
)
@@ -42,6 +45,9 @@ async def get_import_settings():
"phash_threshold": row.phash_threshold,
"download_rate_limit_seconds": row.download_rate_limit_seconds,
"download_validate_files": row.download_validate_files,
"download_schedule_default_seconds": row.download_schedule_default_seconds,
"download_event_retention_days": row.download_event_retention_days,
"download_failure_warning_threshold": row.download_failure_warning_threshold,
})
@@ -73,6 +79,25 @@ async def update_import_settings():
{"error": "download_validate_files must be a boolean"}
), 400
# FC-3d scheduling knobs — bounds validation.
def _bad_int(name: str, lo: int, hi: int):
return jsonify(
{"error": f"{name} must be an integer in [{lo}, {hi}]"}
), 400
if "download_schedule_default_seconds" in body:
v = body["download_schedule_default_seconds"]
if not isinstance(v, int) or isinstance(v, bool) or v < 60 or v > 86400:
return _bad_int("download_schedule_default_seconds", 60, 86400)
if "download_event_retention_days" in body:
v = body["download_event_retention_days"]
if not isinstance(v, int) or isinstance(v, bool) or v < 1 or v > 3650:
return _bad_int("download_event_retention_days", 1, 3650)
if "download_failure_warning_threshold" in body:
v = body["download_failure_warning_threshold"]
if not isinstance(v, int) or isinstance(v, bool) or v < 1 or v > 100:
return _bad_int("download_failure_warning_threshold", 1, 100)
async with get_session() as session:
row = (
await session.execute(select(ImportSettings).where(ImportSettings.id == 1))
+62
View File
@@ -82,6 +82,68 @@ async def test_system_stats_shape(client):
assert status in body["tasks"]
# --- FC-3d: scheduling knobs ----------------------------------------------
@pytest.mark.asyncio
async def test_get_includes_scheduling_defaults(client):
resp = await client.get("/api/settings/import")
body = await resp.get_json()
assert body["download_schedule_default_seconds"] == 28800
assert body["download_event_retention_days"] == 90
assert body["download_failure_warning_threshold"] == 5
@pytest.mark.asyncio
async def test_patch_scheduling_defaults(client):
resp = await client.patch("/api/settings/import", json={
"download_schedule_default_seconds": 7200,
"download_event_retention_days": 30,
"download_failure_warning_threshold": 3,
})
assert resp.status_code == 200
body = await resp.get_json()
assert body["download_schedule_default_seconds"] == 7200
assert body["download_event_retention_days"] == 30
assert body["download_failure_warning_threshold"] == 3
@pytest.mark.asyncio
async def test_patch_rejects_below_min_interval(client):
resp = await client.patch(
"/api/settings/import",
json={"download_schedule_default_seconds": 30},
)
assert resp.status_code == 400
@pytest.mark.asyncio
async def test_patch_rejects_above_max_interval(client):
resp = await client.patch(
"/api/settings/import",
json={"download_schedule_default_seconds": 90000},
)
assert resp.status_code == 400
@pytest.mark.asyncio
async def test_patch_rejects_zero_retention_days(client):
resp = await client.patch(
"/api/settings/import",
json={"download_event_retention_days": 0},
)
assert resp.status_code == 400
@pytest.mark.asyncio
async def test_patch_rejects_zero_failure_threshold(client):
resp = await client.patch(
"/api/settings/import",
json={"download_failure_warning_threshold": 0},
)
assert resp.status_code == 400
@pytest.mark.asyncio
async def test_system_stats_integrity_counts(client, db):
from backend.app.models import ImageRecord