simbiot-harness/tests/test_repo_sync.py

160 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""t-261007082307-2e2294: репо-гигиена — fetch origin в дневном тике (hermetic).
Запуск: ./venv/bin/python tests/test_repo_sync.py
Проверяет: _repo_sync_check пишет RepoSynced с behind/ahead (fetch в git-
песочнице), один замер на день (повтор не плодит записи), при behind>0
первый день эскалации НЕТ, второй день подряд — EscalationRaised (счётчик
дней из истории стора, рестарт демона его не сбрасывает), ошибка fetch
не валит шаг (behind=unknown честно фиксируется), env-порог
SIMBIOT_REPO_SYNC_MAX_AGE_DAYS управляет чувствительностью.
"""
import json
import os
import shutil
import subprocess
import sys
import tempfile
from datetime import timedelta
# ── изоляция: своя БД в /tmp, git-песочница вместо реального репо ──
_DST = "/tmp/simbiot-test-reposync.db"
for p in (_DST, _DST + "-wal", _DST + "-shm"):
if os.path.exists(p):
os.unlink(p)
os.environ["SIMBIOT_DB"] = _DST
os.environ["SIMBIOT_REPO_SYNC_MAX_AGE_DAYS"] = "2"
sys.path.insert(0, "/root/simbiot-harness")
from harness import es, paths # noqa: E402
from harness.levels import LEVELS # noqa: E402
from harness import scheduler # noqa: E402
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "")
if not cond:
fails.append(name)
def _git(cwd, *args):
r = subprocess.run(["git"] + list(args), cwd=cwd,
capture_output=True, timeout=30, check=True)
return r.stdout.decode().strip()
def synced_events(limit=200):
out = []
for e in es.get_events(limit=limit):
if e["event_type"] == "RepoSynced":
e["data"] = json.loads(e["data"])
out.append(e)
return out
def escalations(limit=500):
return [e for e in es.get_events(limit=limit)
if e["event_type"] == "EscalationRaised"]
def make_sandbox():
"""bare origin + клон: init запушен, локально ещё 1 незапушенный коммит."""
base = tempfile.mkdtemp(prefix="simbiot-reposync-")
origin = os.path.join(base, "origin.git")
_git(base, "init", "--bare", "-b", "master", origin)
clone = os.path.join(base, "clone")
_git(base, "clone", origin, clone)
for d in (clone,):
_git(d, "config", "user.email", "test@simbiot.local")
_git(d, "config", "user.name", "simbiot-test")
_git(clone, "commit", "--allow-empty", "-m", "init")
_git(clone, "push", "origin", "master")
_git(clone, "commit", "--allow-empty", "-m", "local-only") # ahead=1
return clone, origin, base
es.init_db()
clone, origin, base = make_sandbox()
_real_root = paths.REPO_ROOT
paths.REPO_ROOT = clone # песочница вместо реального репо
try:
# ── 1. happy path: fetch ok, behind=0/ahead=1, RepoSynced записан ──
res = scheduler._repo_sync_check(LEVELS["day"])
check("fetch ok", res.get("fetch_ok") is True)
check("behind=0", res.get("behind") == 0)
check("ahead=1 (незапушенный коммит виден)", res.get("ahead") == 1)
evs = synced_events(50)
check("RepoSynced записан", len(evs) == 1, f"{len(evs)}")
today = evs[0]["aggregate_id"].split(":", 1)[1]
check("aggregate_id = repo-sync:<сегодня>",
evs[0]["aggregate_id"] == "repo-sync:" +
scheduler._now().strftime("%Y-%m-%d"))
# ── 2. повтор в тот же день — замер тот же, записей не больше ──
again = scheduler._repo_sync_check(LEVELS["day"])
check("повтор: те же числа", again.get("behind") == 0 and again.get("ahead") == 1)
check("повтор в тот же день не плодит записи", len(synced_events(50)) == 1)
# ── 3. upstream ушёл вперёд: behind=1, эскалации ещё НЕТ (1-й день) ──
work = os.path.join(base, "worktree")
_git(base, "clone", origin, work)
_git(work, "config", "user.email", "test@simbiot.local")
_git(work, "config", "user.name", "simbiot-test")
_git(work, "commit", "--allow-empty", "-m", "upstream change")
_git(work, "push", "origin", "master")
res3 = scheduler._repo_sync_check(LEVELS["day"])
check("behind=1 увиден", res3.get("behind") == 1)
check("эскалации НЕТ в первый день", len(escalations()) == 0)
# ── 4. вчера тоже был behind>0 → сегодня 2-й подряд → эскалация ──
yday = (scheduler._now() - timedelta(days=1)).strftime("%Y-%m-%d")
eid, _v = es.append_event("harness", f"repo-sync:{yday}", "RepoSynced", {
"ts": yday + "T06:00:00+00:00", "fetch_ok": True,
"behind": 1, "ahead": 1})
db = es._conn()
db.execute("UPDATE events SET timestamp=? WHERE id=?",
(yday + "T06:00:00.000000+00:00", eid))
db.commit()
db.close()
res4 = scheduler._repo_sync_check(LEVELS["day"])
check("behind=1 стабилен", res4.get("behind") == 1)
escs = escalations()
check("эскалация ПОДНЯТА на 2-й день подряд", len(escs) == 1, f"{len(escs)}")
if escs:
reason = json.loads(escs[-1]["data"]).get("reason", "")
check("причина упоминает behind и 2 дня",
"behind=1" in reason and "2 дн" in reason, reason[:120])
finally:
paths.REPO_ROOT = _real_root
shutil.rmtree(base, ignore_errors=True)
# ── 5. ошибка fetch не валит шаг (свежая БД: дедуп дня не мешает) ──
_DST2 = "/tmp/simbiot-test-reposync2.db"
for p in (_DST2, _DST2 + "-wal", _DST2 + "-shm"):
if os.path.exists(p):
os.unlink(p)
es.DB_PATH = _DST2
es._signature_schema_ready = False # латч на процесс: новая БД требует миграции
es.init_db()
paths.REPO_ROOT = "/nonexistent-repo-dir"
try:
res5 = scheduler._repo_sync_check(LEVELS["day"])
check("fetch-fail: fetch_ok=False, не упал", res5.get("fetch_ok") is False)
evs5 = synced_events(50)
check("fetch-fail зафиксирован событием", len(evs5) == 1)
if evs5:
check("behind=unknown (None), не выдуманный 0",
evs5[0]["data"].get("behind") is None)
check("ошибка сохранена", bool(evs5[0]["data"].get("error")))
finally:
paths.REPO_ROOT = _real_root
# ── 6. env-порог читается при импорте ──
check("порог = 2 из env", scheduler._REPO_SYNC_MAX_AGE_DAYS == 2)
print()
print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}")
sys.exit(1 if fails else 0)