Spaces:
Running
Running
SilverElixir commited on
Commit ·
8fe8000
1
Parent(s): 3ffabb6
Close aiohttp session leaks: no session without key, stub session in unit tests
Browse files- eval/RESULTS.md +49 -23
- eval/harness_eval.py +7 -7
- lumen_commands.py +4 -0
- tests/test_bot_commands.py +11 -0
eval/RESULTS.md
CHANGED
|
@@ -16,29 +16,31 @@
|
|
| 16 |
- **base** — текущий `AGENTS.md`;
|
| 17 |
- **weak** — `AGENTS.md` из двух строк (контроль: влияет ли файл вообще);
|
| 18 |
- **candidate** — `AGENTS.md`, предложенный редактором по отчётам о базовых
|
| 19 |
-
прогонах: 0 удалённых строк, добавлен раздел «Shell and tooling rules»
|
|
|
|
|
|
|
| 20 |
|
| 21 |
## Итоги
|
| 22 |
|
| 23 |
-
| Задача | base | candidate | weak |
|
| 24 |
-
|---|---|---|---|
|
| 25 |
-
| format-newlines-br | решено 311с | решено 675с | решено 155с |
|
| 26 |
-
| format-table-separator-bound | решено 337с | решено 469с | решено 192с |
|
| 27 |
-
| presplit-glued-bullets | решено 212с | решено 248с | решено 175с |
|
| 28 |
-
| state-corrupt-index | решено 262с | решено 512с | решено 282с |
|
| 29 |
-
| state-dirty-counters | решено 353с | решено 406с | решено 344с |
|
| 30 |
-
| streaming-catchup-throttle | решено 340с | решено 537с | решено 348с |
|
| 31 |
-
| tiktok-dns-private-ips | решено 580с | решено 501с | решено 319с |
|
| 32 |
-
|
| 33 |
-
| Показатель | base | candidate | weak |
|
| 34 |
-
|---|---|---|---|
|
| 35 |
-
| Решено | 7/7 | 7/7 | 7/7 |
|
| 36 |
-
| Правил тесты | 0 | 0 | 0 |
|
| 37 |
-
| Задачи с unix-командами | 5 | 1 | 4 |
|
| 38 |
-
| Запустил pyflakes | 7/7 | 7/7 | 0/7 |
|
| 39 |
-
| Запустил pytest | 7/7 | 7/7 | 7/7 |
|
| 40 |
-
| Суммарное время | 2395с | 3348с | 1814с |
|
| 41 |
-
| Всего команд в шелле | 77 | 76 | 48 |
|
| 42 |
|
| 43 |
## Что это значит
|
| 44 |
|
|
@@ -57,11 +59,34 @@
|
|
| 57 |
`python -m pytest`. Правило отменяет гейт. В harness2 за это отвечают валидаторы
|
| 58 |
в каркасе (у них есть свои проверки дерева и защита от утечек в рубрику), в моей
|
| 59 |
упрощённой версии их нет.
|
| 60 |
-
5. **
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
`bash -lc 'pyflakes bot.py lumen_*.py ...'`: то есть обернул в bash, чтобы
|
| 62 |
получить линуксовые глобы. Причина в самом `AGENTS.md`: строка гейта записана в
|
| 63 |
CI-виде (линукс), а хост — Windows, PowerShell.
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
## Вывод
|
| 66 |
|
| 67 |
Harness²-подход («агент сам чинит свою обвязку») на этом репозитории не окупается:
|
|
@@ -70,8 +95,9 @@ Harness²-подход («агент сам чинит свою обвязку»
|
|
| 70 |
|
| 71 |
Что стоит забрать:
|
| 72 |
|
| 73 |
-
-
|
| 74 |
-
|
|
|
|
| 75 |
- «уроки с доказательствами» (вторая рекурсия harness2) — дёшево, без каркаса:
|
| 76 |
после провала записать в файл правило со ссылкой на провал;
|
| 77 |
- сам измеритель `eval/` — им можно проверять любое будущее изменение `AGENTS.md`.
|
|
|
|
| 16 |
- **base** — текущий `AGENTS.md`;
|
| 17 |
- **weak** — `AGENTS.md` из двух строк (контроль: влияет ли файл вообще);
|
| 18 |
- **candidate** — `AGENTS.md`, предложенный редактором по отчётам о базовых
|
| 19 |
+
прогонах: 0 удалённых строк, добавлен раздел «Shell and tooling rules»;
|
| 20 |
+
- **fixed** — `AGENTS.md` с двумя ручными строками про PowerShell и форму гейта
|
| 21 |
+
для Windows (вариант, который в итоге предложен владельцу).
|
| 22 |
|
| 23 |
## Итоги
|
| 24 |
|
| 25 |
+
| Задача | base | candidate | fixed | weak |
|
| 26 |
+
|---|---|---|---|---|
|
| 27 |
+
| format-newlines-br | решено 311с | решено 675с | решено 329с | решено 155с |
|
| 28 |
+
| format-table-separator-bound | решено 337с | решено 469с | решено 265с | решено 192с |
|
| 29 |
+
| presplit-glued-bullets | решено 212с | решено 248с | решено 309с | решено 175с |
|
| 30 |
+
| state-corrupt-index | решено 262с | решено 512с | решено 242с | решено 282с |
|
| 31 |
+
| state-dirty-counters | решено 353с | решено 406с | решено 328с | решено 344с |
|
| 32 |
+
| streaming-catchup-throttle | решено 340с | решено 537с | решено 529с | решено 348с |
|
| 33 |
+
| tiktok-dns-private-ips | решено 580с | решено 501с | решено 360с | решено 319с |
|
| 34 |
+
|
| 35 |
+
| Показатель | base | candidate | fixed | weak |
|
| 36 |
+
|---|---|---|---|---|
|
| 37 |
+
| Решено | 7/7 | 7/7 | 7/7 | 7/7 |
|
| 38 |
+
| Правил тесты | 0 | 0 | 0 | 0 |
|
| 39 |
+
| Задачи с unix-командами | 5 | 1 | 0 | 4 |
|
| 40 |
+
| Запустил pyflakes | 7/7 | 7/7 | 7/7 | 0/7 |
|
| 41 |
+
| Запустил pytest | 7/7 | 7/7 | 7/7 | 7/7 |
|
| 42 |
+
| Суммарное время | 2395с | 3348с | 2362с | 1814с |
|
| 43 |
+
| Всего команд в шелле | 77 | 76 | 67 | 48 |
|
| 44 |
|
| 45 |
## Что это значит
|
| 46 |
|
|
|
|
| 59 |
`python -m pytest`. Правило отменяет гейт. В harness2 за это отвечают валидаторы
|
| 60 |
в каркасе (у них есть свои проверки дерева и защита от утечек в рубрику), в моей
|
| 61 |
упрощённой версии их нет.
|
| 62 |
+
5. **Две ручные строки бьют автоправку.** Вариант `fixed` (две строки про PowerShell
|
| 63 |
+
и форма гейта для Windows) дал 0 unix-команд, 67 команд в шелле вместо 77 и время
|
| 64 |
+
2362с против 2395с у базы — то есть меньше шума и быстрее, при полном гейте.
|
| 65 |
+
Автоправка на том же месте дала 1 unix-команду и +40% времени.
|
| 66 |
+
6. **Остаточная unix-команда в candidate — показательна.** Агент выполнил гейт как
|
| 67 |
`bash -lc 'pyflakes bot.py lumen_*.py ...'`: то есть обернул в bash, чтобы
|
| 68 |
получить линуксовые глобы. Причина в самом `AGENTS.md`: строка гейта записана в
|
| 69 |
CI-виде (линукс), а хост — Windows, PowerShell.
|
| 70 |
|
| 71 |
+
## Побочно найденное и починенное
|
| 72 |
+
|
| 73 |
+
Полный прогон тестов на Windows заканчивался мусором в stderr: «Unclosed client
|
| 74 |
+
session» от aiohttp и «--- Logging error ---» от логгера, который пытается
|
| 75 |
+
отформатировать запись уже во время выхода интерпретатора. Код в��зврата pytest при
|
| 76 |
+
этом был 0 — шум вводил в заблуждение и меня (и агента: он тратил по 2-3 команды на
|
| 77 |
+
разбор «упавшего» прогона).
|
| 78 |
+
|
| 79 |
+
Причины и правки:
|
| 80 |
+
|
| 81 |
+
- `tests/test_bot_commands.py`: юнит-тесты дёргали настоящую `aiohttp.ClientSession`
|
| 82 |
+
бота. Добавлена автофикстура, подменяющая `_get_http_session` заглушкой.
|
| 83 |
+
- `lumen_commands._fish_audio_tts_bytes`: сессия открывалась до проверки ключа
|
| 84 |
+
OpenRouter. Теперь при пустом ключе возвращается None сразу, без сессии (в
|
| 85 |
+
продукте ветка и так выключена флагом FISH_AUDIO_ENABLED, правка убирает мусор из
|
| 86 |
+
юнитов и лишнюю сессию в принципе).
|
| 87 |
+
|
| 88 |
+
После правок: 999 тестов прошли, stderr пуст, код возврата 0.
|
| 89 |
+
|
| 90 |
## Вывод
|
| 91 |
|
| 92 |
Harness²-подход («агент сам чинит свою обвязку») на этом репозитории не окупается:
|
|
|
|
| 95 |
|
| 96 |
Что стоит забрать:
|
| 97 |
|
| 98 |
+
- две ручные строки в `AGENTS.md` про PowerShell и форму гейта для Windows —
|
| 99 |
+
измеримый выигрыш: 0 unix-команд вместо 5, меньше команд в шелле, гейт не
|
| 100 |
+
пострадал;
|
| 101 |
- «уроки с доказательствами» (вторая рекурсия harness2) — дёшево, без каркаса:
|
| 102 |
после провала записать в файл правило со ссылкой на провал;
|
| 103 |
- сам измеритель `eval/` — им можно проверять любое будущее изменение `AGENTS.md`.
|
eval/harness_eval.py
CHANGED
|
@@ -466,6 +466,7 @@ def run_one(task_id: str, variant: str, out_dir: Path, agents_md: Path | None, m
|
|
| 466 |
|
| 467 |
log(f"песочница {task.id} ({variant}) на {task.base[:8]}")
|
| 468 |
sandbox = prepare(task, sandbox, agents_md)
|
|
|
|
| 469 |
|
| 470 |
pre_fail, _ = pytest_state(sandbox)
|
| 471 |
targets = compute_targets(task)["fail_to_pass"]
|
|
@@ -522,7 +523,7 @@ def run_one(task_id: str, variant: str, out_dir: Path, agents_md: Path | None, m
|
|
| 522 |
result_file = out_dir / f"{task.id}.{variant}.json"
|
| 523 |
result_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 524 |
log(f"итог: {'решено' if result.solved else 'не решено'} ({duration}с) -> {result_file}")
|
| 525 |
-
check_repo_untouched()
|
| 526 |
return payload
|
| 527 |
|
| 528 |
|
|
@@ -558,16 +559,15 @@ def cmd_batch(args: argparse.Namespace) -> int:
|
|
| 558 |
return 0
|
| 559 |
|
| 560 |
|
| 561 |
-
def check_repo_untouched() -> None:
|
| 562 |
"""Сторож: настоящий репозиторий не должен меняться во время прогона."""
|
| 563 |
try:
|
| 564 |
-
status = git(REPO, "status", "--porcelain")
|
| 565 |
except RuntimeError as exc:
|
| 566 |
log(f"ВНИМАНИЕ: не смог проверить состояние репозитория: {exc}")
|
| 567 |
return
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
]
|
| 571 |
if unexpected:
|
| 572 |
log(f"ВНИМАНИЕ: репозиторий изменён во время прогона: {unexpected[:5]}")
|
| 573 |
|
|
@@ -741,7 +741,7 @@ def main() -> int:
|
|
| 741 |
run.set_defaults(func=cmd_run)
|
| 742 |
|
| 743 |
batch = sub.add_parser("batch", help="прогнать несколько задач одним вариантом")
|
| 744 |
-
batch.add_argument("--variant", default="base", choices=["base", "candidate", "weak"])
|
| 745 |
batch.add_argument("--tasks", default=None, help="список id через запятую")
|
| 746 |
batch.add_argument("--agents-md", default=None)
|
| 747 |
batch.add_argument("--model", default=None)
|
|
|
|
| 466 |
|
| 467 |
log(f"песочница {task.id} ({variant}) на {task.base[:8]}")
|
| 468 |
sandbox = prepare(task, sandbox, agents_md)
|
| 469 |
+
repo_status_before = [line for line in git(REPO, "status", "--porcelain").splitlines() if line]
|
| 470 |
|
| 471 |
pre_fail, _ = pytest_state(sandbox)
|
| 472 |
targets = compute_targets(task)["fail_to_pass"]
|
|
|
|
| 523 |
result_file = out_dir / f"{task.id}.{variant}.json"
|
| 524 |
result_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 525 |
log(f"итог: {'решено' if result.solved else 'не решено'} ({duration}с) -> {result_file}")
|
| 526 |
+
check_repo_untouched(repo_status_before)
|
| 527 |
return payload
|
| 528 |
|
| 529 |
|
|
|
|
| 559 |
return 0
|
| 560 |
|
| 561 |
|
| 562 |
+
def check_repo_untouched(before: list[str]) -> None:
|
| 563 |
"""Сторож: настоящий репозиторий не должен меняться во время прогона."""
|
| 564 |
try:
|
| 565 |
+
status = [line for line in git(REPO, "status", "--porcelain").splitlines() if line]
|
| 566 |
except RuntimeError as exc:
|
| 567 |
log(f"ВНИМАНИЕ: не смог проверить состояние репозитория: {exc}")
|
| 568 |
return
|
| 569 |
+
before_keys = {line[3:] for line in before}
|
| 570 |
+
unexpected = [line for line in status if line[3:] not in before_keys]
|
|
|
|
| 571 |
if unexpected:
|
| 572 |
log(f"ВНИМАНИЕ: репозиторий изменён во время прогона: {unexpected[:5]}")
|
| 573 |
|
|
|
|
| 741 |
run.set_defaults(func=cmd_run)
|
| 742 |
|
| 743 |
batch = sub.add_parser("batch", help="прогнать несколько задач одним вариантом")
|
| 744 |
+
batch.add_argument("--variant", default="base", choices=["base", "candidate", "weak", "fixed"])
|
| 745 |
batch.add_argument("--tasks", default=None, help="список id через запятую")
|
| 746 |
batch.add_argument("--agents-md", default=None)
|
| 747 |
batch.add_argument("--model", default=None)
|
lumen_commands.py
CHANGED
|
@@ -187,6 +187,10 @@ async def cmd_draw(message: Message) -> None:
|
|
| 187 |
|
| 188 |
async def _fish_audio_tts_bytes(text: str) -> bytes | None:
|
| 189 |
import bot
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
session = await bot._get_http_session()
|
| 191 |
return await _lumen_fish_audio_tts_bytes(
|
| 192 |
session, text,
|
|
|
|
| 187 |
|
| 188 |
async def _fish_audio_tts_bytes(text: str) -> bytes | None:
|
| 189 |
import bot
|
| 190 |
+
if not bot.OPENROUTER_API_KEY:
|
| 191 |
+
# Без ключа всё равно вернётся None: сессию не открываем (в тестах
|
| 192 |
+
# она оставалась жить до выхода интерпретатора и печатала в stderr).
|
| 193 |
+
return None
|
| 194 |
session = await bot._get_http_session()
|
| 195 |
return await _lumen_fish_audio_tts_bytes(
|
| 196 |
session, text,
|
tests/test_bot_commands.py
CHANGED
|
@@ -20,6 +20,17 @@ from tests.bot_test_helpers import (
|
|
| 20 |
)
|
| 21 |
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
def test_cmd_logs_flushes_the_listeners_real_handlers_not_root():
|
| 24 |
# РЕГРЕССИЯ (аудит логирования): cmd_logs раньше флашил
|
| 25 |
# logging.getLogger().handlers — после перехода на QueueHandler/QueueListener
|
|
|
|
| 20 |
)
|
| 21 |
|
| 22 |
|
| 23 |
+
@pytest.fixture(autouse=True)
|
| 24 |
+
def _stub_http_session(monkeypatch):
|
| 25 |
+
# Команды берут общую aiohttp-сессию бота. В юните она не нужна, а без
|
| 26 |
+
# закрытия живёт до выхода интерпретатора и печатает в stderr
|
| 27 |
+
# "Unclosed client session" — из-за этого прогон выглядит упавшим.
|
| 28 |
+
async def fake_get_http_session():
|
| 29 |
+
return SimpleNamespace()
|
| 30 |
+
|
| 31 |
+
monkeypatch.setattr(bot, "_get_http_session", fake_get_http_session)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
def test_cmd_logs_flushes_the_listeners_real_handlers_not_root():
|
| 35 |
# РЕГРЕССИЯ (аудит логирования): cmd_logs раньше флашил
|
| 36 |
# logging.getLogger().handlers — после перехода на QueueHandler/QueueListener
|