Files
versioning/tests/test_maintenance.py
T
retoor 3499f6cda0 Remove encryption; user-data filters; SQLite safety; recovery, retention, scheduler
- No client-side encryption: plaintext content-addressed remote (SHA-256
  names), no backup key, no cryptography dependency (server disk encryption
  is the trust model).
- Filters back user data: images, archives, databases, PDFs accepted; 10 MiB
  cap; binary sniffing removed; temp names hardened (~$, #..#, .temp).
- SQLite zero-error policy: backup-API snapshots + integrity_check, journal
  folding, locked/corrupt loud skips, verified restores.
- Recovery: reindex from manifests, remote adopt, on-demand blob fetch,
  5-day retention + thinning, GC, date-guarded remote purge, metrics.
- Scheduler with WebDAV quota signal and 70% pressure backstop (floor kept).
- 37 tests incl. live-monitor capture safety and DB safety.
2026-10-10 03:41:36 +02:00

104 lines
4.2 KiB
Python

"""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