simbiot-harness/tests/test_aum.py

132 lines
No EOL
6.6 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-260929161138: north-star метрика AUM (hermetic, без LLM/TG/9router).
Запуск: ./venv/bin/python tests/test_aum.py
Проверяет: замер не падает без БД роутера и balances.json (AUM=None),
детерминизм пилота (seed), отбор находит конфигурацию не хуже бейзлайна,
событие AumMeasured мержит policy.aum_target + копит aum_history (кап 120),
build_context инжектит «ЦЕЛЬ (AUM)» всем уровням, CLI aum/aum-pilot/--record
существуют, AumMeasured идемпотентен по aggregate_id.
"""
import io
import json
import os
import sys
import contextlib
# ── изоляция: своя БД в /tmp, роутер и balances заведомо отсутствуют ──
_DST = "/tmp/simbiot-test-aum.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_ROUTER_DB"] = "/nonexistent-router.sqlite"
BAL_TMP = "/tmp/simbiot-test-balances.json"
if os.path.exists(BAL_TMP):
os.unlink(BAL_TMP)
os.environ["SIMBIOT_BALANCES_JSON"] = BAL_TMP
sys.path.insert(0, "/root/simbiot-harness")
from harness import aum, es # noqa: E402
from harness.levels import LEVELS # noqa: E402
from harness.scheduler import build_context # 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)
# ── 1. замер hermetic: не падает, AUM=None, burn=0 ──
m = aum.measure()
check("measure hermetic ok", set(m) >= {"ts", "aum", "burn_24h", "burn_7d", "spend_days"})
check("aum None без файла", m["aum"] is None)
check("burn 0 без роутера", m["burn_7d"] == 0 and m["spend_days"] == {})
# ── 2. balances с данными → total ──
with open(BAL_TMP, "w") as f:
json.dump({"accounts": [{"name": "a", "amount_usd": 12.5},
{"name": "b", "amount_usd": 7.5}]}, f)
m2 = aum.measure()
check("balances→total", m2["aum"] == 20.0)
# ── 3. детерминизм эволюции ──
h = [60., 300., 900., 500., 150., 200.]
e1, e2 = aum.evolve(h), aum.evolve(h)
check("эволюция детерминирована",
e1["best"] == e2["best"] and e1["best_fitness"] == e2["best_fitness"])
check("отбор не хуже бейзлайна", e1["best_fitness"] >= e1["baseline_fitness"] - 1e-6)
check("fitness_history по поколениям", len(e1["fitness_history"]) == e1["generations"])
# ── 4. симуляция: больше burn без отдачи — всегда минус (плейсхолдер честен) ──
s_small = aum.simulate({"volume_mult": 0.2, "flash_share": 0.84}, h, aum.calibrate(h, {"volume_mult": 1.0, "flash_share": 0.84}))
s_big = aum.simulate({"volume_mult": 2.5, "flash_share": 0.84}, h, aum.calibrate(h, {"volume_mult": 1.0, "flash_share": 0.84}))
check("меньше объёма burn → лучше при плейсхолдер-модели",
s_small["mean"] > s_big["mean"])
# ── 5. события AumMeasured: policy мержится, история копится, кап 120 ──
# (повторный aggregate_id легален — каждая версия = новый снапшот; UNIQUE в es
# защищает только тики cycle. Дневная частота обеспечивается идемпотентностью тика.)
es.init_db()
es.append_event("harness", "aum", "AumMeasured", {
"ts": "2026-10-02T00:00:00+00:00", "aum": 20.0, "burn_24h": 1.5,
"burn_7d": 10.0,
"policy": {"aum_target": "ЦЕЛЬ: AUM↑ (тест)"}}
)
model = es.run_projections()
check("policy.aum_target записан", "AUM" in (model["policy"].get("aum_target") or ""))
check("aum_history ведётся", len(model.get("aum_history", [])) == 1)
es.append_event("harness", "aum", "AumMeasured", {
"ts": "2026-10-02T06:00:00+00:00", "aum": 21.0, "burn_24h": 1.0,
"burn_7d": 11.0, "policy": {"aum_target": "ЦЕЛЬ: AUM↑ (тест2)"}})
model = es.run_projections()
check("повторный снапшот апендится в историю",
len(model.get("aum_history", [])) == 2)
check("aum_target перезаписан свежим",
"тест2" in (model["policy"].get("aum_target") or ""))
for i in range(118):
es.append_event("harness", "aum", "AumMeasured", {
"ts": f"2026-10-03T{i:02d}:00:00+00:00", "aum": float(i),
"burn_24h": float(i), "burn_7d": float(i),
"policy": {"aum_target": f"x{i}"}})
model = es.run_projections()
check("кап истории 120", len(model.get("aum_history", [])) == 120)
# ── 6. build_context инжектит цель всем LLM-уровням ──
ok_ctx = True
for name in ("min", "hour", "day"):
ctx = build_context(LEVELS[name], model, batch=[])
if "ЦЕЛЬ (AUM)" not in ctx:
ok_ctx = False
print(f" ({name}: цель не в контексте)")
check("ЦЕЛЬ (AUM) в контексте min/hour/day", ok_ctx)
# ── 7. summary содержит фильтр +1% и burn ──
summ = aum.build_policy_aum_summary(m2, {"evolution": e1})
check("summary: фильтр +1% и burn", "+1%" in summ and "burn" in summ)
# ── 8. CLI: команды существуют и работают hermetic ──
env = dict(os.environ, SIMBIOT_DB=_DST, SIMBIOT_ROUTER_DB="/nonexistent-router.sqlite",
SIMBIOT_BALANCES_JSON=BAL_TMP)
import subprocess
r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harness/cli.py", "aum"],
capture_output=True, text=True, timeout=60, env=env)
check("cli aum ok", r.returncode == 0 and "AUM" in r.stdout, r.stderr[:200])
r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harness/cli.py",
"aum", "--record"], capture_output=True, text=True, timeout=60, env=env)
check("cli aum --record ok -> стор читает цель",
r.returncode == 0 and "OK: AumMeasured" in r.stdout)
model = es.run_projections()
check("aum_target в сторе после --record",
"AUM" in (model["policy"].get("aum_target") or ""))
r = subprocess.run(["/root/simbiot-harness/venv/bin/python", "/root/simbiot-harness/cli.py", "aum-pilot"],
capture_output=True, text=True, timeout=120, env=env)
check("cli aum-pilot ok", r.returncode == 0 and "вердикт" in r.stdout, r.stderr[:200])
print()
print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}")
sys.exit(1 if fails else 0)