# -*- coding: utf-8 -*- """Сжатие стора с подписью (harness/compact.py) — hermetic: свой стор, свой ключ. Запуск: ./venv/bin/python tests/test_compact.py Проверяет: шум сек-уровня уходит в архив, значимое остаётся (алерт/команды сек-уровня, задачи, сообщения, голова стора); полный verify до и после — OK, мост сверен с подписанным StoreCompacted; подмена моста (убрать/ добавить подпись, снести таблицу) → FAIL; архив: sha256 и подпись каждой строки, правка строки → FAIL; повторное сжатие; VACUUM ужимает файл; проекции после сжатия живы. """ import gzip import json import os import sqlite3 import sys import tempfile sys.path.insert(0, "/root/simbiot-harness") _tmp = tempfile.mkdtemp(prefix="simbiot_compact_") os.environ["SIMBIOT_DB"] = os.path.join(_tmp, "harness.db") os.environ["SIMBIOT_AUDIT_KEY_DIR"] = os.path.join(_tmp, "audit") os.environ["SIMBIOT_ARCHIVE_DIR"] = os.path.join(_tmp, "archive") from harness import audit, compact, es es.init_db() fails = [] def check(name, cond, extra=""): print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "") if not cond: fails.append(name) def fill(seq0, n): for i in range(seq0, seq0 + n): es.append_tick("sec", i) es.append_event("cycle", f"reflex:{i}", "ReflexObserved", {"level": "sec", "seq": i, "load1": 0.5}) if i % 50 == 0: es.append_event("cycle", f"reflex-alert:{i}", "ReflexObserved", {"level": "sec", "seq": i, "alert": "load 20 >= 12"}) if i % 40 == 0: es.append_event("tg", f"m-{i}", "MessageReceived", {"update_id": i, "text": "привет"}) es.append_tick("min", i) def types(): db = sqlite3.connect(os.environ["SIMBIOT_DB"]) out = dict(db.execute("SELECT event_type, COUNT(*) FROM events GROUP BY 1").fetchall()) db.close() return out fill(1000, 300) # legacy-граница: неподписанная шумовая строка (как писал старый код до # рестарта демонов) и за ней подписанная с пустым prev — кейс id 466633 _db = sqlite3.connect(os.environ["SIMBIOT_DB"]) _db.execute("INSERT INTO events (aggregate,aggregate_id,event_type,data,timestamp,version) " "VALUES ('cycle','reflex:legacy','ReflexObserved','{\"level\":\"sec\"}',?,1)", (es.datetime.now(es.timezone.utc).isoformat(),)) _db.commit(); _db.close() es.append_event("tg", "m-after-legacy", "MessageReceived", {"update_id": 1, "text": "после legacy"}) fill(2000, 60) before = types() v0 = audit.verify_events() check("verify до сжатия — OK", v0["ok"] and v0["checked"] > 600, v0) plan = compact.plan(older_than_hours=0) check("plan: кандидаты — шум сек-уровня", plan["candidates"] >= 590, plan) r = compact.compact(older_than_hours=0, log=lambda *_: None) after = types() check("сжатие: строки ушли в архив и удалены", r["rows"] == r["removed"] >= 590, r) check("значимое осталось: MessageReceived", after.get("MessageReceived") == before["MessageReceived"]) check("значимое осталось: мин-тики", sqlite3.connect(os.environ["SIMBIOT_DB"]).execute( "SELECT COUNT(*) FROM events WHERE event_type='CycleTicked' " "AND json_extract(data,'$.level')='min'").fetchone()[0] == 10) check("маркер legacy-границы в мосте", sqlite3.connect(os.environ["SIMBIOT_DB"]).execute( "SELECT COUNT(*) FROM compaction_bridge WHERE sig LIKE 'boundary:%'").fetchone()[0] >= 1) check("значимое осталось: ReflexObserved с алертом", sqlite3.connect(os.environ["SIMBIOT_DB"]).execute( "SELECT COUNT(*) FROM events WHERE event_type='ReflexObserved' " "AND json_extract(data,'$.alert') IS NOT NULL").fetchone()[0] == 8) check("StoreCompacted записан", after.get("StoreCompacted") == 1) check("мост не пуст (ссылки остающихся на удалённых)", r["bridge_count"] > 0, r["bridge_count"]) v1 = audit.verify_events() check("verify после сжатия — OK", v1["ok"] and v1.get("compactions") == 1, v1) check("в цепочке меньше строк", v1["total"] < v0["total"] - 500, (v0["total"], v1["total"])) arch = os.path.join(os.environ["SIMBIOT_ARCHIVE_DIR"], r["archive"]) va = compact.verify_archive(arch, r["archive_sha256"]) check("архив: sha256 и подписи всех строк", va["ok"] and va["rows"] == r["rows"], va) check("архив 0600", oct(os.stat(arch).st_mode & 0o777) == "0o600") # подделка моста db = sqlite3.connect(os.environ["SIMBIOT_DB"]) victim = db.execute("SELECT sig FROM compaction_bridge LIMIT 1").fetchone()[0] db.execute("DELETE FROM compaction_bridge WHERE sig=?", (victim,)); db.commit() check("мост без подписи → FAIL", not audit.verify_events()["ok"]) db.execute("INSERT INTO compaction_bridge VALUES (?, ?)", (victim, r["compaction"])) db.execute("INSERT INTO compaction_bridge VALUES ('forged', ?)", (r["compaction"],)); db.commit() check("мост с лишней подписью → FAIL", not audit.verify_events()["ok"]) db.execute("DELETE FROM compaction_bridge WHERE sig='forged'"); db.commit() check("мост восстановлен → OK", audit.verify_events()["ok"]) saved = db.execute("SELECT sig, compaction FROM compaction_bridge").fetchall() db.execute("DROP TABLE compaction_bridge"); db.commit() check("таблица моста снесена → FAIL", not audit.verify_events()["ok"]) db.execute(compact.BRIDGE_DDL) db.executemany("INSERT INTO compaction_bridge VALUES (?, ?)", saved); db.commit() db.close() check("мост на месте → OK", audit.verify_events()["ok"]) # подделка архива bad = os.path.join(_tmp, "bad.jsonl.gz") with gzip.open(arch, "rt", encoding="utf-8") as f: lines = f.read().splitlines() row = json.loads(lines[5]); row["data"] = row["data"].replace("0.5", "9.9") lines[5] = json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) with gzip.open(bad, "wt", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") vb = compact.verify_archive(bad, r["archive_sha256"]) check("правка строки архива → подпись и sha256 не сходятся", not vb["ok"] and vb["first_bad"] == row["id"] and not vb["sha_ok"], vb) # проекции живы, повторное сжатие es.run_projections() fill(5000, 120) r2 = compact.compact(older_than_hours=0, log=lambda *_: None) v2 = audit.verify_events() check("второе сжатие — verify OK, сжатий 2", r2["removed"] > 200 and v2["ok"] and v2["compactions"] == 2, v2) check("проекции после сжатий", isinstance(es.run_projections(), dict)) # бывшая голова (шум, которую второй проход не тронул) — законный кандидат r3 = compact.compact(older_than_hours=0, log=lambda *_: None) check("третий проход: только бывшая голова", r3["removed"] <= 1, r3) v3 = audit.verify_events() check("verify после третьего сжатия — OK", v3["ok"], v3) vac = compact.vacuum() check("VACUUM ужал файл", vac["after"] < vac["before"], vac) check("verify после VACUUM — OK", audit.verify_events()["ok"]) print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)") sys.exit(1 if fails else 0)