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 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. **Остаточная unix-команда в candidate — показательна.** Агент выполнил гейт как
 
 
 
 
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
- - точечную правку `AGENTS.md` про Windows/PowerShell и правильную форму гейта для
74
- PowerShell (агент тратит ходы на `tail`, `ls` и на линуксовые глобы);
 
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
- unexpected = [
569
- line for line in status.splitlines() if line and not line[3:].startswith("eval/")
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