132 lines
No EOL
6.6 KiB
Python
132 lines
No EOL
6.6 KiB
Python
# -*- 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) |