simbiot-harness/tests/test_kodi_core_tasks.py
Simbiot Harness ebcfe3b9be ядро Коди: эскалация недоделанных задач, голова task_fit, рефлексия, недельный отчёт
Разбор гейта (трассы с 15.10): hour — 0% пустых тиков, min — 6% (15%
токенов уровня) → модель-гейт не нужен. Пустые мин-тики — зацикливание:
«бюджет исчерпан» возвращал S как S, мин брал её снова с нуля (S-задача
33 захвата за 2 ч; 45 из 272 задач ≥ 2 мин-захватов).
- requeue_or_escalate: после 2 захватов на уровне (SIMBIOT_TASK_ATTEMPTS_
  PER_LEVEL) размер растёт выше классов уровня (S→M, к часу, 300 шагов) —
  и для guard «бюджет исчерпан», и для смерти воркера; счётчик попыток
  best effort (сбой → обычный requeue, не ломает путь падения воркера);
- kodi_core — несколько голов (общие: хранение/обучение/промоут/откат,
  nf в модели; SIMBIOT_CORE_<ГОЛОВА>). Голова task_fit: влезет ли S в
  мин-тик (метки — из истории задач); shadow — прогноз перед первым
  мин-захватом, on — уверенное «не влезет» сразу S→M. Честно: на 93
  задачах 5 фолдов 0.58–0.62 при базе 0.52, уверенных 0% — пока инертна;
- рефлексия (как reflexion.py): ≥ 3 расхождений вердикта адресации с
  меткой человека/чата или ≥ 3 S-задач, не влезших в мин → задача часу
  с автодоставкой: разобрать и предложить Alex правило (код не трогать);
- недельный отчёт ядра (понедельник, без LLM, идемпотентно по неделе);
  ночью: train_all → reflect → weekly_report; cli.py core reflect|report|--head.
Тесты: test_task_escalation 8, test_kodi_core_tasks 13, test_kodi_core_
reflect 10, test_kodi_core 28 PASS; полный набор — без регрессий.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:32:41 +00:00

129 lines
6.3 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 -*-
"""Ядро Коди, голова task_fit: «влезет ли S-задача в мин-тик» — hermetic.
Запуск: ./venv/bin/python tests/test_kodi_core_tasks.py
Проверяет: метки из истории (1 — закрыта на min с одного захвата; 0 —
повторы/эскалация/другой уровень; None — исход не ясен, M-задачи);
обучение и чемпион головы; shadow — прогноз пишется, батч не меняется;
on — уверенное «не влезет» → S→M до мин-захвата, «влезет» остаётся;
повторно (после мин-попытки) прогноз не делается; статус по головам.
"""
import hashlib
import json
import os
import sys
import tempfile
sys.path.insert(0, "/root/simbiot-harness")
_tmp = tempfile.mkdtemp(prefix="simbiot_core_tasks_")
os.environ["SIMBIOT_DB"] = os.path.join(_tmp, "harness.db")
os.environ["SIMBIOT_CORE_DIR"] = os.path.join(_tmp, "core")
for _v in ("SIMBIOT_CORE_TASK_FIT", "SIMBIOT_DECISION"):
os.environ.pop(_v, None)
import numpy as np
from harness import es, kodi_core, scheduler
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)
A = np.zeros(768, np.float32); A[0] = 1
B = np.zeros(768, np.float32); B[1] = 1
def fake_embed(texts):
out = []
for t in texts:
rng = np.random.default_rng(int(hashlib.sha1(t.encode()).hexdigest(), 16) % (2 ** 32))
v = (A if "тяжёлая" in t else B) + 0.02 * rng.standard_normal(768).astype(np.float32)
out.append(v / np.linalg.norm(v))
return out
kodi_core.embed = fake_embed
N = [0]
def task(title, size="S", claims=(), done=None, promote=None):
N[0] += 1
tid = f"t-{N[0]:04d}"
es.append_event("task", tid, "TaskSubmitted", {"id": tid, "title": title, "size": size,
"created": f"2026-10-01T00:{N[0] % 60:02d}:00"})
for lv in claims:
es.append_event("task", tid, "TaskClaimed", {"level": lv})
if promote and lv == "min":
es.append_event("task", tid, "TaskPromoted", {"from": "S", "to": "S", "reason": "guard"})
if promote == "esc":
es.append_event("task", tid, "TaskPromoted", {"from": "S", "to": "M", "reason": "эскалация"})
if done:
es.append_event("task", tid, "TaskCompleted", {"result": "ok"})
return tid
# ── метки ──
fit = task("лёгкая: проверь бэкап", claims=["min"], done=True)
loop = task("тяжёлая: найди плашку в портале", claims=["min", "min", "min"], done=True, promote="guard")
esc = task("тяжёлая: миграция", claims=["min", "min", "hour"], done=True, promote="esc")
open_ = task("лёгкая: в работе", claims=["min"])
m_ = task("M-задача", size="M", claims=["hour"], done=True)
H = kodi_core.task_history()
check("метка 1: закрыта на min с одного захвата", kodi_core.task_label(H[fit]) == 1.0)
check("метка 0: три захвата на min", kodi_core.task_label(H[loop]) == 0.0)
check("метка 0: эскалация к часу", kodi_core.task_label(H[esc]) == 0.0)
check("исход не ясен — без метки", kodi_core.task_label(H[open_]) is None)
check("M-задача — не для этой головы", kodi_core.task_label(H[m_]) is None)
# ── обучение ──
for i in range(30):
task(f"лёгкая: мелочь {i}", claims=["min"], done=True)
task(f"тяжёлая: исследование {i}", claims=["min", "min"], done=True, promote="guard")
rep = kodi_core.train(log=lambda *_: None, head="task_fit")
check("обучение task_fit → чемпион", rep["promoted"] and rep["holdout"]["acc"] >= .9, rep)
check("модели голов — в разных каталогах",
os.path.isfile(os.path.join(os.environ["SIMBIOT_CORE_DIR"], "task_fit", "champion.json")))
# ── shadow: прогноз есть, батч не трогаем ──
heavy = task("тяжёлая: новая про портал")
light = task("лёгкая: новая проверка")
model = es.run_projections()
batch = [model["tasks"][heavy], model["tasks"][light]]
out = scheduler._core_task_fit(list(batch))
check("shadow: батч не изменён", [t["id"] for t in out] == [heavy, light])
preds = {json.loads(e["data"])["task"]: json.loads(e["data"]) for e in es.get_events(0, 10**6)
if e["event_type"] == "CorePrediction"}
check("shadow: прогнозы записаны (тяжёлая < 0.5 < лёгкая)",
preds[heavy]["p_fit"] < .5 < preds[light]["p_fit"] and not preds[heavy]["act"], preds)
# ── on: уверенное «не влезет» → S→M до клейма ──
os.environ["SIMBIOT_CORE_TASK_FIT"] = "on"
heavy2 = task("тяжёлая: ещё одна")
light2 = task("лёгкая: ещё одна")
model = es.run_projections()
fam = kodi_core.load_champion("task_fit")["nov_q"]
kodi_core.load_champion("task_fit")["nov_q"] = np.asarray([1.0, 1.0]) # всё «знакомо» — проверяем связку
out = scheduler._core_task_fit([model["tasks"][heavy2], model["tasks"][light2]])
t2 = es.run_projections()["tasks"]
check("on: тяжёлая убрана из мин-батча и стала M", [t["id"] for t in out] == [light2]
and t2[heavy2]["size"] == "M", ([t["id"] for t in out], t2[heavy2]["size"]))
check("on: лёгкая осталась S", t2[light2]["size"] == "S")
es.append_event("task", light2, "TaskClaimed", {"level": "min"})
n_before = sum(1 for e in es.get_events(0, 10**6) if e["event_type"] == "CorePrediction")
scheduler._core_task_fit([es.run_projections()["tasks"][light2]])
check("после мин-попытки прогноз не повторяется",
sum(1 for e in es.get_events(0, 10**6) if e["event_type"] == "CorePrediction") == n_before)
os.environ.pop("SIMBIOT_CORE_TASK_FIT")
st = kodi_core.status()
check("статус: обе головы", set(st) == {"addressing", "task_fit"} and st["task_fit"]["champion"], st.keys())
print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
sys.exit(1 if fails else 0)