"""Scheduler, quota signal and capacity backstop.""" import asyncio import time import httpx import pytest from fastapi.testclient import TestClient from versiond import maintenance from versiond.api import create_app from versiond.config import Config, Paths from versiond.remote import quota as remote_quota from test_service import CONFIG, history, wait_for from test_purge import FakeDAV, REMOTE @pytest.fixture def maint_env(tmp_path, monkeypatch): monkeypatch.setenv("VERSIOND_HOME", str(tmp_path / "home")) paths = Paths.resolve() paths.ensure() paths.config_file.write_text(CONFIG + "\n[upload]\nmanifest_interval_seconds = 0.2\n") cfg = Config.load(paths) dav = FakeDAV() project = tmp_path / "proj" project.mkdir() (project / "main.py").write_text("print('hello')\n") app = create_app(cfg, remote_transport=httpx.MockTransport(dav.handler)) with TestClient(app, base_url="http://127.0.0.1:9922") as client: client.headers["Authorization"] = f"Bearer {cfg.api_token()}" app.state.services.uploader.offline_sleep = 0.3 yield client, dav, project, cfg, app.state.services def test_quota_signal_and_fallback(maint_env): client, dav, project, cfg, svc = maint_env directory = client.put("/api/v1/config/remote", json=REMOTE).json()["directory"] async def get_quota(): from versiond.remote import WebDAV settings = svc.uploader.settings() dav_client = WebDAV(settings, transport=svc.uploader.transport) try: return await remote_quota(dav_client, directory) finally: await dav_client.close() used, available = asyncio.run(get_quota()) assert used is not None and used >= 0 and available == dav.capacity_bytes - used async def refresh(): return await maintenance.refresh_usage(svc) usage = asyncio.run(refresh()) assert usage["source"] == "webdav-quota" assert usage["percent"] == round(100 * used / dav.capacity_bytes, 1) assert maintenance.over_limit(svc) is False assert client.get("/health").json()["remote_pressure"] is False def test_pressure_triggers_pass_but_floor_holds(maint_env): client, dav, project, cfg, svc = maint_env directory = client.put("/api/v1/config/remote", json=REMOTE).json()["directory"] client.post("/api/v1/roots", json={"path": str(project)}) wait_for(lambda: history(client, project / "main.py"), timeout=10) # Tiny remote: everything ever uploaded already exceeds 70%. dav.capacity_bytes = 1 result = asyncio.run(maintenance.run_scheduled_pass(svc, "test-pressure")) assert result["pressure"] is True assert result["versions_deleted"] == 0 # all young: the 5-day floor holds assert len(history(client, project / "main.py")) == 1 assert client.get("/health").json()["remote_pressure"] is True assert "remote-over-capacity-limit" in client.get("/health").json()["degraded_roots"] upload = client.get("/api/v1/progress").json()["upload"] assert upload["usage"]["source"] == "webdav-quota" assert upload["usage"]["percent"] >= 70 def test_scheduled_pass_cleans_old_and_reports(maint_env): import sqlite3 client, dav, project, cfg, svc = maint_env client.put("/api/v1/config/remote", json=REMOTE) client.post("/api/v1/roots", json={"path": str(project)}) wait_for(lambda: history(client, project / "main.py"), timeout=10) (project / "main.py").write_text("v2\n") wait_for(lambda: len(history(client, project / "main.py")) == 2, timeout=10) import time as _t _t.sleep(0.5) (project / "main.py").write_text("v3\n") wait_for(lambda: len(history(client, project / "main.py")) == 3, timeout=10) old = time.time() - 10 * 86400 conn = sqlite3.connect(cfg.paths.index_file, timeout=5.0) vids = [v["id"] for v in reversed(history(client, project / "main.py"))] for vid in vids[:2]: # two oldest share one old day: keep daily + latest conn.execute("UPDATE versions SET captured_at = ? WHERE id = ?", (old, vid)) conn.commit() conn.close() result = asyncio.run(maintenance.run_scheduled_pass(svc, "test-scheduled")) assert result["versions_deleted"] == 1 assert len(history(client, project / "main.py")) == 2