hermes-hub/tests/test_a56_review_findings.py
Hermes Team 6f4394113b review(a56): сжатие контекста принято с исправлениями
Проверено исполнением на сервере владельца, а не по отчёту.

Работает. Сжатие вызывается в настоящем пути запроса (local_adapter.py:168) —
разрыв, ради которого писалось задание, закрыт. Замер на живом компрессоре:
5667 токенов на входе, 624 на выходе, 0,11x, экономия 5043 токена за 127,6 с.

Исправлено четыре дефекта.

1. /props и /tokenize запрашивались по адресу с суффиксом /v1. У llama.cpp они
   живут в корне: измерено, /props → 200, /v1/props → 404, то же с /tokenize.
   Адаптер передаёт супервизору именно адрес с /v1, поэтому счёт токенов молча
   падал на посимвольную оценку, и порог сжатия считался от выдуманного числа.
   Адрес нормализуется.

2. Заявленные «сто процентов сохранения фактов» модель не даёт. Замер: 37 из 38,
   97,4%, потерян 001cd1f. Сто процентов получались дописыванием недостающих
   фактов списком — механизм верный, но измеренное число подменялось
   исправленным, а «(100%)» было вписано в сообщение текстом. Теперь полнота
   самой модели сохраняется отдельно и показывается владельцу: иначе ухудшение
   модели осталось бы незамеченным.

3. Итог сжатия считался посимвольно (длина / 3.5) и подавался рядом с настоящим
   числом токенов на входе. Теперь пересчитывается токенизатором сервера, а
   недоступность токенизатора помечается признаком оценки.

4. Порт 8082 был зашит запасным адресом в двух местах вопреки прямому запрету в
   задании. Профиль без адреса теперь даёт состояние «не настроен» с причиной,
   а не молчаливый стук в 8082.

Не выполнено исполнителем: проверка на живом сервере (P0-6.1). Тест на неё
пропускается как негерметичный, замеры выше сделал ревьюер.

649 passed, 2 skipped; ruff чисто; релизный гейт пройден.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 14:32:35 +07:00

233 lines
8.7 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.

"""Проверки по итогам разбора A56 ревьюером.
Три находки, каждая подтверждена измерением на сервере владельца:
1. `/props` и `/tokenize` у llama.cpp живут в корне, а не под `/v1`.
Измерено: `/props` → 200, `/v1/props` → 404; то же с `/tokenize`.
Адаптер передаёт супервизору адрес вида `.../v1`, поэтому счёт токенов
молча падал на посимвольную оценку, и порог сжатия считался от выдуманного
числа.
2. Заявленные «100% сохранения фактов» модель не даёт. Измерено на живом
компрессоре: 5667 токенов на входе, 624 на выходе, 37 фактов из 38 —
97,4%, потерян `001cd1f`. Сто процентов получаются дописыванием
недостающих фактов списком. Механизм верный, но подменять им измеренное
число нельзя: иначе ухудшение модели останется незамеченным.
3. Порт 8082 был зашит в коде как запасной адрес. Задание это прямо
запрещает: сегодня он такой, завтра другой.
"""
from __future__ import annotations
from unittest.mock import patch
import pytest
from antigravity_provider.router.context_compressor import (
CompressionOutcome,
ContextCompressor,
)
from antigravity_provider.router.local_supervisor import LocalSupervisor
# ── Находка 1: адрес /props и /tokenize ──
@pytest.mark.unit
@pytest.mark.parametrize(
"given, expected_root",
[
("http://127.0.0.1:8081/v1", "http://127.0.0.1:8081"),
("http://127.0.0.1:8081/v1/", "http://127.0.0.1:8081"),
("http://127.0.0.1:8081", "http://127.0.0.1:8081"),
("http://server.local:9000/v1", "http://server.local:9000"),
],
)
def test_props_and_tokenize_go_to_the_root(given, expected_root):
assert LocalSupervisor(base_url=given).root_url == expected_root
@pytest.mark.unit
def test_tokenize_request_url_has_no_v1():
supervisor = LocalSupervisor(base_url="http://127.0.0.1:8081/v1")
seen = {}
def _fake_urlopen(req, timeout=None):
seen["url"] = req.full_url
raise RuntimeError("сеть в тесте недоступна")
with patch("urllib.request.urlopen", _fake_urlopen):
supervisor.count_tokens("проверка")
assert seen["url"] == "http://127.0.0.1:8081/tokenize"
@pytest.mark.unit
def test_props_request_url_has_no_v1():
supervisor = LocalSupervisor(base_url="http://127.0.0.1:8081/v1")
seen = {}
def _fake_urlopen(req, timeout=None):
seen["url"] = req.full_url
raise RuntimeError("сеть в тесте недоступна")
with patch("urllib.request.urlopen", _fake_urlopen):
supervisor.query_server_props()
assert seen["url"] == "http://127.0.0.1:8081/props"
@pytest.mark.unit
def test_unmeasured_limit_is_marked_as_such():
"""Недоступный /props даёт запасное значение — и оно обязано быть помечено."""
supervisor = LocalSupervisor(base_url="http://127.0.0.1:8081/v1")
def _fake_urlopen(req, timeout=None):
raise RuntimeError("сервер не отвечает")
with patch("urllib.request.urlopen", _fake_urlopen):
result = supervisor.query_server_props()
assert result.is_measured is False
# ── Находка 2: полнота фактов не подменяется исправленной ──
class _Profile:
custom_base_url = "http://127.0.0.1:9999/v1"
preferred_models = ["compressor"]
auth_config: dict = {}
def _compress_with_summary(summary: str):
payload = {"choices": [{"message": {"content": summary}}], "model": "test-gguf"}
class _Resp:
def read(self):
import json
return json.dumps(payload).encode()
def __enter__(self):
return self
def __exit__(self, *a):
return False
messages = [
{"role": "system", "content": "система"},
{"role": "user", "content": "Правка в коммите 001cd1f, порт 8082, файл /srv/app/main.py"},
{"role": "assistant", "content": "Скорость 107.4 tok/s на версии v0.1.3"},
{"role": "user", "content": "свежее 1"},
{"role": "user", "content": "свежее 2"},
{"role": "user", "content": "свежее 3"},
]
compressor = ContextCompressor()
with patch("urllib.request.urlopen", return_value=_Resp()), \
patch.object(ContextCompressor, "_record_to_shared_memory", lambda self, o: None):
_, outcome = compressor.compress_messages_if_needed(
messages=messages,
target_context_limit=1000,
current_token_count=900,
compressor_profile=_Profile(),
)
return outcome
@pytest.mark.unit
def test_model_retention_is_reported_separately_when_facts_were_added():
# Сводка намеренно теряет часть фактов — их дописывает страховка.
outcome = _compress_with_summary("Кратко: правка внесена, порт 8082.")
assert outcome.status == "SUCCESS"
assert outcome.facts_added_by_safeguard, "страховка обязана была сработать"
assert outcome.model_retention_percent < 100.0
assert outcome.retention_percent == pytest.approx(100.0)
assert "дописано списком" in outcome.status_message
assert f"{outcome.model_retention_percent:.1f}%" in outcome.status_message
@pytest.mark.unit
def test_full_model_retention_is_stated_as_the_model_s_own():
summary = "Коммит 001cd1f, порт 8082, файл /srv/app/main.py, 107.4 tok/s, v0.1.3"
outcome = _compress_with_summary(summary)
assert not outcome.facts_added_by_safeguard
assert outcome.model_retention_percent == pytest.approx(100.0)
assert "самой моделью" in outcome.status_message
@pytest.mark.unit
def test_status_message_never_hardcodes_a_hundred_percent():
import inspect
from antigravity_provider.router import context_compressor
source = inspect.getsource(context_compressor)
assert "(100%)." not in source, "полнота обязана браться из замера, а не из строки"
# ── Находка 3: порт компрессора не зашит ──
@pytest.mark.unit
def test_compressor_port_is_not_hardcoded():
import inspect
from antigravity_provider.router import context_compressor
code_lines = [
line
for line in inspect.getsource(context_compressor).splitlines()
if not line.lstrip().startswith("#")
]
assert not any("127.0.0.1:8082" in line for line in code_lines)
@pytest.mark.unit
def test_profile_without_address_is_unconfigured_not_port_8082(monkeypatch):
monkeypatch.delenv("LOCAL_COMPRESSOR_BASE_URL", raising=False)
class _NoAddress:
custom_base_url = None
preferred_models: list = []
auth_config: dict = {}
_, outcome = ContextCompressor().compress_messages_if_needed(
messages=[{"role": "user", "content": "x"}],
target_context_limit=1000,
current_token_count=900,
compressor_profile=_NoAddress(),
)
assert outcome.status == "UNCONFIGURED"
assert "адрес" in outcome.status_message
# ── Итог сжатия пересчитывается токенизатором ──
@pytest.mark.unit
def test_result_size_is_recounted_by_the_tokenizer():
supervisor = LocalSupervisor(base_url="http://127.0.0.1:8081/v1")
outcome = CompressionOutcome(
status="SUCCESS", status_message="", tokens_before=900, tokens_after=1
)
with patch.object(
ContextCompressor,
"compress_messages_if_needed",
return_value=([{"role": "user", "content": "сводка"}], outcome),
), patch.object(
LocalSupervisor,
"count_tokens",
side_effect=[
type("T", (), {"tokens_count": 900, "is_estimated": False})(),
type("T", (), {"tokens_count": 120, "is_estimated": False})(),
],
):
_, result = supervisor.compress_context_if_needed(
messages=[{"role": "user", "content": "длинная история"}],
target_context_limit=1000,
)
assert result.tokens_after == 120
assert result.tokens_after_is_estimate is False
assert result.saved_tokens == 780