# -*- 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)