# -*- 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]) # ── 9. авто-обновление балансов из API (t-261004000834-ed84f5) — hermetic ── import tempfile tmpdir = tempfile.mkdtemp(prefix="simbiot-aum-") # 9a: файла нет → [] без падения check("refresh_auto: нет файла → []", aum.refresh_auto_balances(path="/nonexistent-balances.json") == []) # 9b: auto-счёт, но ключей нет → значение не пишется, ошибок нет bal_nokey = os.path.join(tmpdir, "b1.json") with open(bal_nokey, "w") as f: json.dump({"accounts": [{"name": "Stripe live balance (available)", "amount_usd": 0.0, "auto": True}]}, f) _saved_efv = aum._env_file_value aum._env_file_value = lambda *a, **k: None # ключей нет try: got = aum.refresh_auto_balances(path=bal_nokey, coop_env=os.path.join(tmpdir, "no.env")) finally: aum._env_file_value = _saved_efv check("refresh_auto: без ключа → [] и файл не тронут", got == [] and json.load(open(bal_nokey))["accounts"][0]["amount_usd"] == 0.0) # 9c: _http_json мокается → суммы пишутся, measured_at/as_of обновляются, # ручные счета не трогаются bal_mock = os.path.join(tmpdir, "b2.json") with open(bal_mock, "w") as f: json.dump({"accounts": [ {"name": "OpenRouter кредиты", "amount_usd": None, "auto": True}, {"name": "Stripe live balance", "amount_usd": None, "auto": True}, {"name": "ручной счёт Alex", "amount_usd": 5.0}], "as_of": "2000-01-01"}, f) with open(os.path.join(tmpdir, "stripe.env"), "w") as f: f.write("STRIPE_LIVE_KEY=sk_test_fake\n") def _fake_http(url, headers): if "openrouter" in url: return {"data": {"total_credits": 100, "total_usage": 40}} return {"livemode": True, "available": [{"currency": "usd", "amount": 12345}]} _saved_http = aum._http_json aum._http_json = _fake_http # hermetic: OPENROUTER_KEY должен приходить из мока .env, а не из реального # .env хоста (на мигрированном инстансе ключа нет — тест падал негерметично) _saved_efv2 = aum._env_file_value # hermetic: OPENROUTER_KEY из реального .env хоста не существует (миграция # инстанса) — даём мок; STRIPE_LIVE_KEY читается из coop_env-файла теста # через настоящий _env_file_value, его не подменяем aum._env_file_value = (lambda path, prefix: "sk-or-test" if prefix == "OPENROUTER_KEY" else _saved_efv2(path, prefix)) try: got2 = aum.refresh_auto_balances(path=bal_mock, coop_env=os.path.join(tmpdir, "stripe.env")) finally: aum._http_json = _saved_http aum._env_file_value = _saved_efv2 d2 = json.load(open(bal_mock)) check("refresh_auto: mock API → 60.0 и 123.45 записаны", len(got2) == 2 and abs(d2["accounts"][0]["amount_usd"] - 60.0) < 0.001 and abs(d2["accounts"][1]["amount_usd"] - 123.45) < 0.001, f"got2={got2} d2={d2}") check("refresh_auto: measured_at у auto, ручной не тронут, as_of обновлён", bool(d2["accounts"][0].get("measured_at")) and d2["accounts"][2]["amount_usd"] == 5.0 and d2["as_of"] != "2000-01-01") # 9d: Stripe livemode=False → None: тестовые деньги в AUM не актив def _fake_http_testmode(url, headers): return {"livemode": False, "available": [{"currency": "usd", "amount": 33524}]} aum._http_json = _fake_http_testmode try: v_test = aum.refresh_stripe_available(coop_env=os.path.join(tmpdir, "stripe.env")) finally: aum._http_json = _saved_http check("stripe: livemode=False → None (не актив)", v_test is None) # ── 10. живая петля burn-7д: router_usage.json (t-261019001211) ── RU = os.path.join(tmpdir, "router_usage.json") for p in (RU,): if os.path.exists(p): os.unlink(p) # 10a: первый замер → новая активная неделя r1 = aum.update_router_usage({"ts": "2026-10-12T00:00:00+00:00", "burn_src": "store-BudgetSpent-оценка", "spend_days": {"2026-10-11": {"cost": 2.0, "reqs": 10}, "2026-10-12": {"cost": 1.0, "reqs": 5}}}, path=RU) w = json.load(open(RU))["weeks"] check("router_usage: первая неделя создана", r1.get("fresh_week") and len(w) == 1 and w[0]["active"] and abs(w[0]["burn_usd"] - 3.0) < 0.001, str(r1)) # 10b: тот же день, окно подросло → накопительная досумма (не дубль полной суммы) r2 = aum.update_router_usage({"ts": "2026-10-12T12:00:00+00:00", "burn_src": "store-BudgetSpent-оценка", "spend_days": {"2026-10-11": {"cost": 2.0, "reqs": 10}, "2026-10-12": {"cost": 2.5, "reqs": 12}}}, path=RU) w = json.load(open(RU))["weeks"] check("router_usage: досумма Δ в той же неделе", not r2.get("fresh_week") and abs(r2.get("delta_usd", -1) - 1.5) < 0.001 and abs(w[0]["burn_usd"] - 4.5) < 0.001, f"r2={r2}") # 10c: скользящее окно сжалось (выпал старый день) — не отнимаем r3 = aum.update_router_usage({"ts": "2026-10-13T00:00:00+00:00", "burn_src": "store-BudgetSpent-оценка", "spend_days": {"2026-10-12": {"cost": 2.5, "reqs": 12}, "2026-10-13": {"cost": 1.0, "reqs": 4}}}, path=RU) w = json.load(open(RU))["weeks"] check("router_usage: сжатие окна не отнимает burn", abs(r3.get("delta_usd", -1)) < 0.001 and abs(w[0]["burn_usd"] - 4.5) < 0.001, f"r3={r3}") # 10d: окно старше 21д → сброс, новое окно, старое закрыто r4 = aum.update_router_usage({"ts": "2026-11-15T00:00:00+00:00", "burn_src": "store-BudgetSpent-оценка", "spend_days": {"2026-11-14": {"cost": 7.0, "reqs": 30}}}, path=RU) w = json.load(open(RU))["weeks"] check("router_usage: недельный сброс окна", r4.get("fresh_week") and len(w) == 2 and not w[0]["active"] and w[1]["active"] and abs(w[1]["burn_usd"] - 7.0) < 0.001, str(r4)) # 10e: битый путь → ошибка в dict, без исключения r5 = aum.update_router_usage({"ts": "2026-11-15T01:00:00+00:00", "spend_days": {"2026-11-14": {"cost": 1.0, "reqs": 3}}}, path="/nonexistent-dir/ru.json") check("router_usage: ошибка тихо в dict", "error" in r5) # 10f: measure() ведёт реестр сам (интеграция: hermetic, реестр в tmp) os.environ["SIMBIOT_ROUTER_USAGE_JSON"] = os.path.join(tmpdir, "ru2.json") _m = aum.measure() check("measure: router_usage пишется из замера", os.path.exists(os.environ["SIMBIOT_ROUTER_USAGE_JSON"])) # ── 11. env-доктор авто-источников (t-261019001211) ── _empty = os.path.join(tmpdir, "empty.env") with open(_empty, "w") as f: f.write("# пусто\n") doc = aum.env_doctor(env_path=_empty, coop_env=os.path.join(tmpdir, "no-coop.env")) check("env_doctor: все три ключа отсутствуют на пустых .env", [m["var"] for m in doc["missing"]] == ["OPENROUTER_KEY", "STRIPE_LIVE_KEY", "SIMBIOT_ROUTER_DB"], str(doc["missing"])) check("env_doctor: шаблон запроса к Alex готов", "OPENROUTER_KEY" in doc["request_to_alex"] and "STRIPE_LIVE_KEY" in doc["request_to_alex"]) _full = os.path.join(tmpdir, "full.env") with open(_full, "w") as f: f.write("OPENROUTER_KEY=sk-test\nSIMBIOT_ROUTER_DB=/tmp/x.sqlite\n") _coop_full = os.path.join(tmpdir, "coop.env") with open(_coop_full, "w") as f: f.write("STRIPE_LIVE_KEY=sk-live-test\n") doc2 = aum.env_doctor(env_path=_full, coop_env=_coop_full) check("env_doctor: полный .env → missing=[]", doc2["missing"] == [] and "не слепа" in doc2["request_to_alex"]) # ── 12. Δ burn неделя-к-неделе (t-261019001211, пункт 4) ── RU2 = os.path.join(tmpdir, "ru_delta.json") with open(RU2, "w") as f: json.dump({"weeks": [{"burn_usd": 10.0, "active": False}, {"burn_usd": 7.5, "active": True}]}, f) check("weekly_burn_delta: −2.5", aum.weekly_burn_delta(path=RU2) == -2.5) os.environ["SIMBIOT_ROUTER_USAGE_JSON"] = RU2 check("weekly_burn_delta: одна неделя → None", aum.weekly_burn_delta(path=os.path.join(tmpdir, "ru-one.json")) is None) s_wd = aum.build_policy_aum_summary({"aum": 26.7, "burn_7d": 7.5, "burn_src": "store-BudgetSpent-оценка"}) check("summary: Δburn неделя-к-неделе в контексте", "Δburn" in s_wd and "-2.5" in s_wd, s_wd) print() print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}") sys.exit(1 if fails else 0)