File size: 4,450 Bytes
d71a8e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
"""test_lumen_devtrace.py — JSONL-след запросов и ответов для разбора багов."""
import asyncio
from types import SimpleNamespace

import bot
import lumen_devtrace
from tests.bot_test_helpers import _FakeAdminRequest


def _enable_trace(monkeypatch, tmp_path):
    monkeypatch.setenv("DEV_TRACE", "1")
    monkeypatch.setenv("DEV_TRACE_PATH", str(tmp_path / "trace.jsonl"))


def test_devtrace_disabled_by_default_writes_nothing(monkeypatch, tmp_path):
    # Сторож: без DEV_TRACE=1 файл не создаётся даже при прямом вызове.
    monkeypatch.delenv("DEV_TRACE", raising=False)
    monkeypatch.setenv("DEV_TRACE_PATH", str(tmp_path / "trace.jsonl"))
    assert lumen_devtrace.record_answer(1, user_text="hi", reply="hello") is False
    assert not (tmp_path / "trace.jsonl").exists()


def test_devtrace_answer_clips_and_redacts_secret(monkeypatch, tmp_path):
    # Защищает разбор глупостей модели (полный маршрут и ответ в файле) и
    # отсутствие секретов в нём: регрессия — сырой токен в trace-файле.
    _enable_trace(monkeypatch, tmp_path)
    monkeypatch.setattr(bot, "BOT_TOKEN", "devtrace-secret-abc")
    ok = lumen_devtrace.record_answer(
        5, user_text="devtrace-secret-abc " + "x" * 2500, route=[("groq", "m1")],
        provider="groq", model="m1", latency_ms=12, reply="y" * 5000,
    )
    assert ok is True
    raw = (tmp_path / "trace.jsonl").read_text(encoding="utf-8")
    assert "devtrace-secret-abc" not in raw
    assert "<REDACTED>" in raw
    entries = lumen_devtrace.read_tail(10)
    assert len(entries) == 1 and entries[0]["kind"] == "answer"
    assert len(entries[0]["user"]) == 2000
    assert len(entries[0]["reply"]) == 4000
    assert entries[0]["route"] == ["groq:m1"]


def test_devtrace_read_tail_returns_last_n_and_skips_broken(monkeypatch, tmp_path):
    # Хвост для /devtrace: последние N и пропуск битых строк вместо 500.
    _enable_trace(monkeypatch, tmp_path)
    for i in range(5):
        assert lumen_devtrace.record_passive(7, f"msg {i}") is True
    with open(tmp_path / "trace.jsonl", "a", encoding="utf-8") as f:
        f.write("{broken\n")
    last = lumen_devtrace.read_tail(2)
    assert [e["text"] for e in last] == ["msg 3", "msg 4"]


def test_devtrace_endpoint_rejects_missing_or_wrong_key(monkeypatch):
    original = bot.ADMIN_PANEL_KEY
    bot.ADMIN_PANEL_KEY = "real-admin-key"
    try:
        result = asyncio.run(bot.devtrace(_FakeAdminRequest(headers={"Authorization": "Bearer wrong"})))
        assert result.status_code == 401
    finally:
        bot.ADMIN_PANEL_KEY = original


def test_devtrace_endpoint_returns_entries(monkeypatch, tmp_path):
    # Сторож: валидный ключ отдаёт записанный след, а не пустой список.
    _enable_trace(monkeypatch, tmp_path)
    original = bot.ADMIN_PANEL_KEY
    bot.ADMIN_PANEL_KEY = "real-admin-key"
    try:
        assert lumen_devtrace.record_failure(9, user_text="q", error="boom") is True
        result = asyncio.run(bot.devtrace(_FakeAdminRequest(headers={"Authorization": "Bearer real-admin-key"})))
        assert result["count"] == 1
        assert result["entries"][0]["kind"] == "failure"
        assert result["enabled"] is True
    finally:
        bot.ADMIN_PANEL_KEY = original


def test_devtrace_passive_hook_writes_background(monkeypatch, tmp_path):
    # Фон группы попадает в след: иначе разбирать влияние контекста не по чему.
    from aiogram.enums import ChatType
    _enable_trace(monkeypatch, tmp_path)
    monkeypatch.setattr(bot, "_should_only_record_passively", lambda *a, **k: True)
    monkeypatch.setattr(bot, "is_guest_message", lambda m: False)
    monkeypatch.setattr(bot, "message_mentions_bot", lambda m: False)
    msg = SimpleNamespace(
        text="фоновый текст", caption=None,
        chat=SimpleNamespace(id=777002, type=ChatType.GROUP),
        from_user=SimpleNamespace(id=456), reply_to_message=None,
    )
    asyncio.run(bot._handle_message_core(msg))
    entries = lumen_devtrace.read_tail(10)
    assert entries and entries[-1]["kind"] == "passive"
    assert "фоновый" in entries[-1]["text"]
    bot.chat_state.pop(777002, None)