Живая петля AUM-метрики: router_usage.json (недельное окно, накопительно) + часовые точки AumMeasured + env-доктор ключей + Δburn нед/нед в policy + спарклайн на публичный дашборд — t-261019001211-d6a921
This commit is contained in:
parent
cbf03ffac2
commit
69ac2cecaa
4 changed files with 281 additions and 1 deletions
15
docs/aum/router_usage.json
Normal file
15
docs/aum/router_usage.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"weeks": [
|
||||||
|
{
|
||||||
|
"week_start": "2026-10-19T01:06:57.981175+00:00",
|
||||||
|
"start_day": "2026-10-19",
|
||||||
|
"burn_usd": 7.7078,
|
||||||
|
"reqs": 243,
|
||||||
|
"days_seen": 2,
|
||||||
|
"src": "недоступен",
|
||||||
|
"active": true,
|
||||||
|
"updated_at": "2026-10-19T01:08:13.330295+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"method": "BudgetSpent стора × блендед-ставка 9.43e-07 у.е./токен; окно 7д, накопительно в рамках недели (t-261019001211)"
|
||||||
|
}
|
||||||
155
harness/aum.py
155
harness/aum.py
|
|
@ -238,6 +238,128 @@ def read_coop_structure():
|
||||||
return {"title": None, "projects": []}
|
return {"title": None, "projects": []}
|
||||||
|
|
||||||
|
|
||||||
|
# ───────────── живая петля burn-7д: router_usage.json (t-261019001211) ─────────────
|
||||||
|
# Слепота AUM-метрики: burn живёт только в тексте policy и теряет историю
|
||||||
|
# при недельном скольжении. Файл docs/aum/router_usage.json — компактный реестр
|
||||||
|
# «неделя → замер» с ежегодным сбросом окна: каждая запись — сумма расхода
|
||||||
|
# с момента прошлого замера (окно кратно ~7д, ровно одна активная неделя).
|
||||||
|
# Идёмпотентность: замер того же дня не создаёт новую запись, а дополняет
|
||||||
|
# активное окно (докупка ΔB = sum_7d − уже_записанное). Так скользящее окно
|
||||||
|
# BudgetSpent-стора превращается в стабильный накопительный реестр: виден
|
||||||
|
# и фактический burn недели, и Δ-тренд неделя-к-неделе.
|
||||||
|
|
||||||
|
ROUTER_USAGE_JSON = os.environ.get(
|
||||||
|
"SIMBIOT_ROUTER_USAGE_JSON",
|
||||||
|
os.path.join(os.path.dirname(BALANCES_JSON), "router_usage.json"))
|
||||||
|
|
||||||
|
|
||||||
|
def update_router_usage(snap: dict = None, path: str = None):
|
||||||
|
"""Дополнить реестр router_usage.json свежим замером burn (7д).
|
||||||
|
|
||||||
|
Вход — снапшот aum.measure() (или собирается сам). Логика:
|
||||||
|
- если активной записи нет или её окно старше 21д — новая запись;
|
||||||
|
- если окно той же недели — досчитать Δburn от уже записанного
|
||||||
|
(скользящее окно BudgetSpent суммирует 7д заново, реестр — накопительно);
|
||||||
|
- окно ровно одно активное; история капится 260 записями (5 лет).
|
||||||
|
Ошибки не бросает (best effort, как и весь AUM-контур) — возвращает dict.
|
||||||
|
"""
|
||||||
|
path = path or os.environ.get("SIMBIOT_ROUTER_USAGE_JSON", ROUTER_USAGE_JSON)
|
||||||
|
snap = snap or measure()
|
||||||
|
days = snap.get("spend_days") or {}
|
||||||
|
sum7 = round(sum(v.get("cost", 0) for v in days.values()), 4)
|
||||||
|
today = snap.get("ts", "")[:10]
|
||||||
|
try:
|
||||||
|
from datetime import date
|
||||||
|
try:
|
||||||
|
today_d = date.fromisoformat(today)
|
||||||
|
except ValueError:
|
||||||
|
today_d = date.today()
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
reg = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
reg = {"weeks": []}
|
||||||
|
weeks = reg.setdefault("weeks", [])
|
||||||
|
cur = weeks[-1] if weeks else None
|
||||||
|
stale = True
|
||||||
|
if cur and cur.get("active") and cur.get("start_day"):
|
||||||
|
try:
|
||||||
|
stale = (today_d - date.fromisoformat(cur["start_day"])).days > 21
|
||||||
|
except ValueError:
|
||||||
|
stale = True
|
||||||
|
fresh = False
|
||||||
|
if (not cur or not cur.get("active") or stale):
|
||||||
|
# закрываем прошлое окно и заводим новое (сброс недели)
|
||||||
|
if cur and cur.get("active"):
|
||||||
|
cur["active"] = False
|
||||||
|
cur["closed_at"] = snap.get("ts")
|
||||||
|
cur = {"week_start": snap.get("ts"), "start_day": today,
|
||||||
|
"burn_usd": 0.0, "reqs": 0, "days_seen": 0,
|
||||||
|
"src": snap.get("burn_src"), "active": True}
|
||||||
|
weeks.append(cur)
|
||||||
|
fresh = True
|
||||||
|
# накопительная досумма поверх уже записанного в этом окне
|
||||||
|
prev = float(cur.get("burn_usd") or 0)
|
||||||
|
delta = round(sum7 - prev, 4)
|
||||||
|
if delta < 0: # скользящее окно выпало из старых дней — не отнимаем
|
||||||
|
delta = 0.0
|
||||||
|
cur["burn_usd"] = round(prev + delta, 4)
|
||||||
|
cur["reqs"] = max(int(cur.get("reqs") or 0),
|
||||||
|
sum(v.get("reqs", 0) for v in days.values()))
|
||||||
|
cur["days_seen"] = max(int(cur.get("days_seen") or 0), len(days))
|
||||||
|
cur["src"] = snap.get("burn_src") or cur.get("src")
|
||||||
|
cur["updated_at"] = snap.get("ts")
|
||||||
|
if len(weeks) > 260:
|
||||||
|
reg["weeks"] = weeks[-260:]
|
||||||
|
reg["method"] = ("BudgetSpent стора × блендед-ставка "
|
||||||
|
f"{BURN_RATE_PER_TOKEN:.2e} у.е./токен; окно 7д, "
|
||||||
|
"накопительно в рамках недели (t-261019001211)")
|
||||||
|
tmp = path + ".tmp"
|
||||||
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(reg, f, ensure_ascii=False, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
os.replace(tmp, path) # атомарно: читатели не увидят полуфайл
|
||||||
|
return {"path": path, "week_start": cur.get("week_start"),
|
||||||
|
"burn_usd": cur["burn_usd"], "delta_usd": delta,
|
||||||
|
"fresh_week": fresh}
|
||||||
|
except Exception as e:
|
||||||
|
return {"path": path, "error": str(e)[:200]}
|
||||||
|
|
||||||
|
|
||||||
|
# ────────── env-доктор авто-источников AUM (t-261019001211, пункт 2) ──────────
|
||||||
|
# balances.json сейчас слеп: OPENROUTER_KEY и STRIPE_LIVE_KEY живут на другом
|
||||||
|
# хосте (KE-SREGISTRY). Чтобы запрос к Alex не был «сделай что-то», доктор
|
||||||
|
# собирает точный список: каких переменных в каком .env не хватает и что
|
||||||
|
# именно каждая открывает в метрике.
|
||||||
|
|
||||||
|
ENV_KEYS_NEEDED = (
|
||||||
|
# (переменная, файл, что открывает в AUM-петле)
|
||||||
|
("OPENROUTER_KEY", "simbiot-harness/.env",
|
||||||
|
"кредиты OpenRouter (auto-баланс №1, сейчас 26.7 фикс с 2026-10-04)"),
|
||||||
|
("STRIPE_LIVE_KEY", "/root/coop/.env",
|
||||||
|
"Stripe live available (auto-баланс, подписки $99/мес)"),
|
||||||
|
("SIMBIOT_ROUTER_DB", "simbiot-harness/.env",
|
||||||
|
"живая БД 9router (usageHistory): точный burn вместо блендед-оценки"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def env_doctor(env_path: str = None, coop_env: str = "/root/coop/.env"):
|
||||||
|
"""Какие ключи отсутствуют + готовый шаблон запроса к Alex (без значений)."""
|
||||||
|
env_path = env_path or _paths.env_path()
|
||||||
|
missing = []
|
||||||
|
for var, where, why in ENV_KEYS_NEEDED:
|
||||||
|
path = coop_env if var == "STRIPE_LIVE_KEY" else env_path
|
||||||
|
present = _env_file_value(path, var) is not None
|
||||||
|
if not present:
|
||||||
|
missing.append({"var": var, "env_file": path, "opens": why})
|
||||||
|
request = ("Чтобы петля AUM видела деньги без ручного ввода, добавь в .env:\n"
|
||||||
|
+ "\n".join(f" {m['var']}=… # в {m['env_file']} — {m['opens']}"
|
||||||
|
for m in missing)) if missing else \
|
||||||
|
"Все ключи авто-источников на месте — петля AUM не слепа."
|
||||||
|
return {"missing": missing, "request_to_alex": request,
|
||||||
|
"checked_at": datetime.now(timezone.utc).isoformat(timespec="seconds")}
|
||||||
|
|
||||||
|
|
||||||
# ────────────────────────── замер ──────────────────────────
|
# ────────────────────────── замер ──────────────────────────
|
||||||
|
|
||||||
def measure():
|
def measure():
|
||||||
|
|
@ -258,6 +380,15 @@ def measure():
|
||||||
if not spend: # БД роутера недоступна → не молчим, а считаем из стора
|
if not spend: # БД роутера недоступна → не молчим, а считаем из стора
|
||||||
spend = read_store_spend(days=7)
|
spend = read_store_spend(days=7)
|
||||||
spend_src = "store-BudgetSpent-оценка" if spend else "недоступен"
|
spend_src = "store-BudgetSpent-оценка" if spend else "недоступен"
|
||||||
|
# живая петля burn-7д (t-261019001211): каждый замер дополняет реестр
|
||||||
|
# router_usage.json (накопительное окно недели) — контур не теряет историю
|
||||||
|
if os.environ.get("SIMBIOT_AUM_USAGE_REGISTRY", "1") not in ("0", "false", "no"):
|
||||||
|
_snap = {"ts": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"spend_days": spend, "burn_src": spend_src}
|
||||||
|
try:
|
||||||
|
update_router_usage(_snap)
|
||||||
|
except Exception:
|
||||||
|
pass # реестр — best effort: замер важнее и идёт всегда
|
||||||
days_iter = list(spend.values())
|
days_iter = list(spend.values())
|
||||||
return {
|
return {
|
||||||
"ts": datetime.now(timezone.utc).isoformat(),
|
"ts": datetime.now(timezone.utc).isoformat(),
|
||||||
|
|
@ -368,6 +499,25 @@ def run_pilot(m: dict) -> dict:
|
||||||
"note": "вход — суточный burn 9router; выход — рекомендации у.е. расхода"}
|
"note": "вход — суточный burn 9router; выход — рекомендации у.е. расхода"}
|
||||||
|
|
||||||
|
|
||||||
|
def weekly_burn_delta(path: str = None):
|
||||||
|
"""Δ burn последняя-прошлая неделя из router_usage.json (у.е. или None).
|
||||||
|
|
||||||
|
Пункт 4 петли (t-261019001211): манифесту дня нужен не только абсолют
|
||||||
|
burn, но и тренд — растём или падаем относительно прошлой недели.
|
||||||
|
"""
|
||||||
|
path = path or os.environ.get("SIMBIOT_ROUTER_USAGE_JSON", ROUTER_USAGE_JSON)
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
weeks = json.load(f).get("weeks", [])
|
||||||
|
if len(weeks) < 2:
|
||||||
|
return None
|
||||||
|
cur, prev = weeks[-1], weeks[-2]
|
||||||
|
return round(float(cur.get("burn_usd") or 0)
|
||||||
|
- float(prev.get("burn_usd") or 0), 2)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def build_policy_aum_summary(m: dict, pilot: dict = None) -> str:
|
def build_policy_aum_summary(m: dict, pilot: dict = None) -> str:
|
||||||
"""Однострочник для policy/контекста всех уровней."""
|
"""Однострочник для policy/контекста всех уровней."""
|
||||||
aum = m.get("aum")
|
aum = m.get("aum")
|
||||||
|
|
@ -376,13 +526,16 @@ def build_policy_aum_summary(m: dict, pilot: dict = None) -> str:
|
||||||
"docs/aum/balances.json)")
|
"docs/aum/balances.json)")
|
||||||
ev = (pilot or {}).get("evolution") or {}
|
ev = (pilot or {}).get("evolution") or {}
|
||||||
tail = f"; последний отбор: {ev.get('verdict', 'не запускался')}" if pilot else ""
|
tail = f"; последний отбор: {ev.get('verdict', 'не запускался')}" if pilot else ""
|
||||||
|
wd = weekly_burn_delta()
|
||||||
|
trend = (f"; Δburn к прошлой неделе: {'+' if wd >= 0 else ''}{wd} у.е."
|
||||||
|
if wd is not None else "")
|
||||||
src = m.get("burn_src") or "9router-db"
|
src = m.get("burn_src") or "9router-db"
|
||||||
burn = (f"≈{m.get('burn_7d', 0)} (оценка: BudgetSpent стора × блендед-ставка; "
|
burn = (f"≈{m.get('burn_7d', 0)} (оценка: BudgetSpent стора × блендед-ставка; "
|
||||||
f"живая БД 9router недоступна)" if src.startswith("store")
|
f"живая БД 9router недоступна)" if src.startswith("store")
|
||||||
else f"{m.get('burn_7d', 0)}")
|
else f"{m.get('burn_7d', 0)}")
|
||||||
return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов "
|
return (f"ЦЕЛЬ: максимизация долгосрочной AUM (sum всех измеримых ресурсов "
|
||||||
f"на длинной дистанции). {part}; burn 9router 7д="
|
f"на длинной дистанции). {part}; burn 9router 7д="
|
||||||
f"{burn} у.е. (прямой минус к AUM). Фильтр любого "
|
f"{burn} у.е.{trend} (прямой минус к AUM). Фильтр любого "
|
||||||
f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; "
|
f"решения и задачи: «добавляет ≥ +1% к AUM в горизонте год+?»; "
|
||||||
f"короткий доход, съедающий долгосрочную базу — ниже приоритетом."
|
f"короткий доход, съедающий долгосрочную базу — ниже приоритетом."
|
||||||
f"{tail}")
|
f"{tail}")
|
||||||
|
|
@ -1256,6 +1256,26 @@ def process_level(level_cfg, model: dict, seq: int = None):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # замер — best effort, не валить тик
|
pass # замер — best effort, не валить тик
|
||||||
|
|
||||||
|
# живая петля AUM-метрики (t-261019001211): дневной снапшот — раз в сутки
|
||||||
|
# (см. выше), часовой дописывает внутрисуточные точки в aum_history —
|
||||||
|
# спарклайн дашборда оживает, а не обновляется раз в день. Идемпотентно
|
||||||
|
# по aggregate_id=aum:hour:<UTC-час>: повторный тик часа не задублирует.
|
||||||
|
if level_cfg.name == "hour":
|
||||||
|
try:
|
||||||
|
from . import aum as _aum
|
||||||
|
snap = _aum.measure()
|
||||||
|
hour_key = snap["ts"][:13] # 2026-10-19T01
|
||||||
|
ev = es.append_event("harness", f"hour:{hour_key}", "AumMeasured", {
|
||||||
|
"ts": snap["ts"], "aum": snap["aum"],
|
||||||
|
"burn_24h": snap["burn_24h"], "burn_7d": snap["burn_7d"],
|
||||||
|
"policy": {"aum_target": _aum.build_policy_aum_summary(snap)},
|
||||||
|
})
|
||||||
|
if ev:
|
||||||
|
_log(f"aum: часовая точка {hour_key} записана "
|
||||||
|
f"(burn_24h={snap['burn_24h']})")
|
||||||
|
except Exception:
|
||||||
|
pass # замер — best effort, не валить тик
|
||||||
|
|
||||||
# ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается
|
# ГЕЙТ ВАЖНОСТИ (e-260930062242/2510/2607/2741): LLM-тик запускается
|
||||||
# только если есть событие ≥ порога уровня; тихий такт отрабатывает
|
# только если есть событие ≥ порога уровня; тихий такт отрабатывает
|
||||||
# детерминированной механикой (как sec) — без расхода кредитов роутера.
|
# детерминированной механикой (как sec) — без расхода кредитов роутера.
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,98 @@ finally:
|
||||||
aum._http_json = _saved_http
|
aum._http_json = _saved_http
|
||||||
check("stripe: livemode=False → None (не актив)", v_test is None)
|
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()
|
||||||
print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}")
|
print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}")
|
||||||
sys.exit(1 if fails else 0)
|
sys.exit(1 if fails else 0)
|
||||||
Loading…
Reference in a new issue