chore: move to top level
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
Vendored
+37
File diff suppressed because one or more lines are too long
@@ -0,0 +1,368 @@
|
||||
import asyncio
|
||||
|
||||
from takopi.exec_bridge import (
|
||||
extract_session_id,
|
||||
prepare_telegram,
|
||||
resolve_resume_session,
|
||||
truncate_for_telegram,
|
||||
)
|
||||
|
||||
|
||||
def test_extract_session_id_finds_uuid_v7() -> None:
|
||||
uuid = "019b66fc-64c2-7a71-81cd-081c504cfeb2"
|
||||
text = f"resume: `{uuid}`"
|
||||
|
||||
assert extract_session_id(text) == uuid
|
||||
|
||||
|
||||
def test_extract_session_id_requires_resume_line() -> None:
|
||||
uuid = "019b66fc-64c2-7a71-81cd-081c504cfeb2"
|
||||
text = f"here is a uuid {uuid}"
|
||||
|
||||
assert extract_session_id(text) is None
|
||||
|
||||
|
||||
def test_extract_session_id_uses_last_resume_line() -> None:
|
||||
uuid_first = "019b66fc-64c2-7a71-81cd-081c504cfeb2"
|
||||
uuid_last = "123e4567-e89b-12d3-a456-426614174000"
|
||||
text = f"resume: `{uuid_first}`\n\nresume: `{uuid_last}`"
|
||||
|
||||
assert extract_session_id(text) == uuid_last
|
||||
|
||||
|
||||
def test_extract_session_id_ignores_malformed_resume_line() -> None:
|
||||
text = "resume: not-a-uuid"
|
||||
|
||||
assert extract_session_id(text) is None
|
||||
|
||||
|
||||
def test_resolve_resume_session_prefers_message_text() -> None:
|
||||
uuid_message = "123e4567-e89b-12d3-a456-426614174000"
|
||||
uuid_reply = "019b66fc-64c2-7a71-81cd-081c504cfeb2"
|
||||
|
||||
assert (
|
||||
resolve_resume_session(
|
||||
f"resume: `{uuid_message}`", f"resume: `{uuid_reply}`"
|
||||
)
|
||||
== uuid_message
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_resume_session_uses_reply_when_missing() -> None:
|
||||
uuid_reply = "019b66fc-64c2-7a71-81cd-081c504cfeb2"
|
||||
|
||||
assert resolve_resume_session("no resume here", f"resume: `{uuid_reply}`") == uuid_reply
|
||||
|
||||
|
||||
def test_truncate_for_telegram_preserves_resume_line() -> None:
|
||||
uuid = "019b66fc-64c2-7a71-81cd-081c504cfeb2"
|
||||
md = ("x" * 10_000) + f"\nresume: `{uuid}`"
|
||||
|
||||
out = truncate_for_telegram(md, 400)
|
||||
|
||||
assert len(out) <= 400
|
||||
assert uuid in out
|
||||
assert out.rstrip().endswith(f"resume: `{uuid}`")
|
||||
|
||||
|
||||
def test_truncate_for_telegram_keeps_last_non_empty_line() -> None:
|
||||
md = "intro\n\n" + ("x" * 500) + "\nlast line"
|
||||
|
||||
out = truncate_for_telegram(md, 120)
|
||||
|
||||
assert len(out) <= 120
|
||||
assert out.rstrip().endswith("last line")
|
||||
|
||||
|
||||
def test_prepare_telegram_drops_entities_on_truncate() -> None:
|
||||
md = ("**bold** " * 200).strip()
|
||||
|
||||
rendered, entities = prepare_telegram(md, limit=40)
|
||||
|
||||
assert len(rendered) <= 40
|
||||
assert entities is None
|
||||
|
||||
|
||||
class _FakeBot:
|
||||
def __init__(self) -> None:
|
||||
self._next_id = 1
|
||||
self.send_calls: list[dict] = []
|
||||
self.edit_calls: list[dict] = []
|
||||
self.delete_calls: list[dict] = []
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
chat_id: int,
|
||||
text: str,
|
||||
reply_to_message_id: int | None = None,
|
||||
disable_notification: bool | None = False,
|
||||
entities: list[dict] | None = None,
|
||||
parse_mode: str | None = None,
|
||||
) -> dict:
|
||||
self.send_calls.append(
|
||||
{
|
||||
"chat_id": chat_id,
|
||||
"text": text,
|
||||
"reply_to_message_id": reply_to_message_id,
|
||||
"disable_notification": disable_notification,
|
||||
"entities": entities,
|
||||
"parse_mode": parse_mode,
|
||||
}
|
||||
)
|
||||
msg_id = self._next_id
|
||||
self._next_id += 1
|
||||
return {"message_id": msg_id}
|
||||
|
||||
async def edit_message_text(
|
||||
self,
|
||||
chat_id: int,
|
||||
message_id: int,
|
||||
text: str,
|
||||
entities: list[dict] | None = None,
|
||||
parse_mode: str | None = None,
|
||||
) -> dict:
|
||||
self.edit_calls.append(
|
||||
{
|
||||
"chat_id": chat_id,
|
||||
"message_id": message_id,
|
||||
"text": text,
|
||||
"entities": entities,
|
||||
"parse_mode": parse_mode,
|
||||
}
|
||||
)
|
||||
return {"message_id": message_id}
|
||||
|
||||
async def delete_message(self, chat_id: int, message_id: int) -> bool:
|
||||
self.delete_calls.append({"chat_id": chat_id, "message_id": message_id})
|
||||
return True
|
||||
|
||||
|
||||
class _FakeRunner:
|
||||
def __init__(self, *, answer: str, saw_agent_message: bool = True) -> None:
|
||||
self._answer = answer
|
||||
self._saw_agent_message = saw_agent_message
|
||||
|
||||
async def run_serialized(self, *_args, **_kwargs) -> tuple[str, str, bool]:
|
||||
return ("019b66fc-64c2-7a71-81cd-081c504cfeb2", self._answer, self._saw_agent_message)
|
||||
|
||||
|
||||
class _FakeClock:
|
||||
def __init__(self, start: float = 0.0) -> None:
|
||||
self._now = start
|
||||
|
||||
def __call__(self) -> float:
|
||||
return self._now
|
||||
|
||||
def set(self, value: float) -> None:
|
||||
self._now = value
|
||||
|
||||
|
||||
class _FakeRunnerWithEvents:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
events: list[dict],
|
||||
times: list[float],
|
||||
clock: _FakeClock,
|
||||
answer: str = "ok",
|
||||
session_id: str = "019b66fc-64c2-7a71-81cd-081c504cfeb2",
|
||||
) -> None:
|
||||
self._events = events
|
||||
self._times = times
|
||||
self._clock = clock
|
||||
self._answer = answer
|
||||
self._session_id = session_id
|
||||
|
||||
async def run_serialized(self, *_args, **kwargs) -> tuple[str, str, bool]:
|
||||
on_event = kwargs.get("on_event")
|
||||
if on_event is not None:
|
||||
for when, event in zip(self._times, self._events, strict=False):
|
||||
self._clock.set(when)
|
||||
await on_event(event)
|
||||
await asyncio.sleep(0)
|
||||
return (self._session_id, self._answer, True)
|
||||
|
||||
|
||||
def test_final_notify_sends_loud_final_message() -> None:
|
||||
from takopi.exec_bridge import BridgeConfig, _handle_message
|
||||
|
||||
bot = _FakeBot()
|
||||
runner = _FakeRunner(answer="ok")
|
||||
cfg = BridgeConfig(
|
||||
bot=bot, # type: ignore[arg-type]
|
||||
runner=runner, # type: ignore[arg-type]
|
||||
chat_id=123,
|
||||
final_notify=True,
|
||||
startup_msg="",
|
||||
max_concurrency=1,
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
_handle_message(
|
||||
cfg,
|
||||
chat_id=123,
|
||||
user_msg_id=10,
|
||||
text="hi",
|
||||
resume_session=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(bot.send_calls) == 2
|
||||
assert bot.send_calls[0]["disable_notification"] is True
|
||||
assert bot.send_calls[1]["disable_notification"] is False
|
||||
|
||||
|
||||
def test_new_final_message_forces_notification_when_too_long_to_edit() -> None:
|
||||
from takopi.exec_bridge import BridgeConfig, _handle_message
|
||||
|
||||
bot = _FakeBot()
|
||||
runner = _FakeRunner(answer="x" * 10_000)
|
||||
cfg = BridgeConfig(
|
||||
bot=bot, # type: ignore[arg-type]
|
||||
runner=runner, # type: ignore[arg-type]
|
||||
chat_id=123,
|
||||
final_notify=False,
|
||||
startup_msg="",
|
||||
max_concurrency=1,
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
_handle_message(
|
||||
cfg,
|
||||
chat_id=123,
|
||||
user_msg_id=10,
|
||||
text="hi",
|
||||
resume_session=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(bot.send_calls) == 2
|
||||
assert bot.send_calls[0]["disable_notification"] is True
|
||||
assert bot.send_calls[1]["disable_notification"] is False
|
||||
|
||||
|
||||
def test_progress_edits_are_rate_limited() -> None:
|
||||
from takopi.exec_bridge import BridgeConfig, _handle_message
|
||||
|
||||
bot = _FakeBot()
|
||||
clock = _FakeClock()
|
||||
events = [
|
||||
{
|
||||
"type": "item.started",
|
||||
"item": {
|
||||
"id": "item_0",
|
||||
"type": "command_execution",
|
||||
"command": "echo 1",
|
||||
"status": "in_progress",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "item.completed",
|
||||
"item": {
|
||||
"id": "item_0",
|
||||
"type": "command_execution",
|
||||
"command": "echo 1",
|
||||
"exit_code": 0,
|
||||
"status": "completed",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "item.started",
|
||||
"item": {
|
||||
"id": "item_1",
|
||||
"type": "command_execution",
|
||||
"command": "echo 2",
|
||||
"status": "in_progress",
|
||||
},
|
||||
},
|
||||
]
|
||||
runner = _FakeRunnerWithEvents(
|
||||
events=events,
|
||||
times=[0.2, 0.4, 1.2],
|
||||
clock=clock,
|
||||
)
|
||||
cfg = BridgeConfig(
|
||||
bot=bot, # type: ignore[arg-type]
|
||||
runner=runner, # type: ignore[arg-type]
|
||||
chat_id=123,
|
||||
final_notify=True,
|
||||
startup_msg="",
|
||||
max_concurrency=1,
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
_handle_message(
|
||||
cfg,
|
||||
chat_id=123,
|
||||
user_msg_id=10,
|
||||
text="hi",
|
||||
resume_session=None,
|
||||
clock=clock,
|
||||
progress_edit_every=1.0,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(bot.edit_calls) == 1
|
||||
|
||||
|
||||
def test_bridge_flow_sends_progress_edits_and_final_resume() -> None:
|
||||
from takopi.exec_bridge import BridgeConfig, _handle_message
|
||||
|
||||
bot = _FakeBot()
|
||||
clock = _FakeClock()
|
||||
events = [
|
||||
{
|
||||
"type": "item.started",
|
||||
"item": {
|
||||
"id": "item_0",
|
||||
"type": "command_execution",
|
||||
"command": "echo ok",
|
||||
"status": "in_progress",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "item.completed",
|
||||
"item": {
|
||||
"id": "item_0",
|
||||
"type": "command_execution",
|
||||
"command": "echo ok",
|
||||
"exit_code": 0,
|
||||
"status": "completed",
|
||||
},
|
||||
},
|
||||
]
|
||||
session_id = "123e4567-e89b-12d3-a456-426614174000"
|
||||
runner = _FakeRunnerWithEvents(
|
||||
events=events,
|
||||
times=[0.0, 2.1],
|
||||
clock=clock,
|
||||
answer="done",
|
||||
session_id=session_id,
|
||||
)
|
||||
cfg = BridgeConfig(
|
||||
bot=bot, # type: ignore[arg-type]
|
||||
runner=runner, # type: ignore[arg-type]
|
||||
chat_id=123,
|
||||
final_notify=True,
|
||||
startup_msg="",
|
||||
max_concurrency=1,
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
_handle_message(
|
||||
cfg,
|
||||
chat_id=123,
|
||||
user_msg_id=42,
|
||||
text="do it",
|
||||
resume_session=None,
|
||||
clock=clock,
|
||||
progress_edit_every=1.0,
|
||||
)
|
||||
)
|
||||
|
||||
assert bot.send_calls[0]["reply_to_message_id"] == 42
|
||||
assert "working" in bot.send_calls[0]["text"]
|
||||
assert len(bot.edit_calls) >= 1
|
||||
assert session_id in bot.send_calls[-1]["text"]
|
||||
assert "resume:" in bot.send_calls[-1]["text"].lower()
|
||||
assert len(bot.delete_calls) == 1
|
||||
@@ -0,0 +1,102 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from takopi.exec_render import ExecProgressRenderer, render_event_cli
|
||||
|
||||
|
||||
def _loads(lines: str) -> list[dict]:
|
||||
return [json.loads(line) for line in lines.strip().splitlines() if line.strip()]
|
||||
|
||||
|
||||
FIXTURE_PATH = Path(__file__).resolve().parent / "fixtures" / "codex.jsonl"
|
||||
|
||||
SAMPLE_STREAM = """
|
||||
{"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}
|
||||
{"type":"turn.started"}
|
||||
{"type":"item.completed","item":{"id":"item_0","type":"reasoning","text":"**Searching for README files**"}}
|
||||
{"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","aggregated_output":"","status":"in_progress"}}
|
||||
{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","aggregated_output":"2025-09-11\\nAGENTS.md\\nCHANGELOG.md\\ncliff.toml\\ncodex-cli\\ncodex-rs\\ndocs\\nexamples\\nflake.lock\\nflake.nix\\nLICENSE\\nnode_modules\\nNOTICE\\npackage.json\\npnpm-lock.yaml\\npnpm-workspace.yaml\\nPNPM.md\\nREADME.md\\nscripts\\nsdk\\ntmp\\n","exit_code":0,"status":"completed"}}
|
||||
{"type":"item.completed","item":{"id":"item_2","type":"reasoning","text":"**Checking repository root for README**"}}
|
||||
{"type":"item.completed","item":{"id":"item_3","type":"agent_message","text":"Yep — there’s a `README.md` in the repository root."}}
|
||||
{"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"output_tokens":122}}
|
||||
"""
|
||||
|
||||
|
||||
def test_render_event_cli_sample_stream() -> None:
|
||||
last_turn = None
|
||||
out: list[str] = []
|
||||
for evt in _loads(SAMPLE_STREAM):
|
||||
last_turn, lines = render_event_cli(evt, last_turn)
|
||||
out.extend(lines)
|
||||
|
||||
assert out == [
|
||||
"thread started",
|
||||
"turn started",
|
||||
"0. **Searching for README files**",
|
||||
"1. ▸ `bash -lc ls`",
|
||||
"1. ✓ `bash -lc ls`",
|
||||
"2. **Checking repository root for README**",
|
||||
"assistant:",
|
||||
" Yep — there’s a `README.md` in the repository root.",
|
||||
"turn completed",
|
||||
]
|
||||
|
||||
|
||||
def test_render_event_cli_real_run_fixture() -> None:
|
||||
events = _loads(FIXTURE_PATH.read_text(encoding="utf-8"))
|
||||
last_turn = None
|
||||
out: list[str] = []
|
||||
for evt in events:
|
||||
last_turn, lines = render_event_cli(evt, last_turn)
|
||||
out.extend(lines)
|
||||
|
||||
print("\n".join(out))
|
||||
|
||||
assert out[0] == "thread started"
|
||||
assert "turn started" in out
|
||||
assert any(line.startswith("0. ▸ `") for line in out)
|
||||
assert any(line.startswith("0. ✓ `") for line in out)
|
||||
assert "assistant:" in out
|
||||
assert any("takopi" in line for line in out)
|
||||
assert out[-1] == "turn completed"
|
||||
|
||||
|
||||
def test_progress_renderer_renders_progress_and_final() -> None:
|
||||
r = ExecProgressRenderer(max_actions=5)
|
||||
for evt in _loads(SAMPLE_STREAM):
|
||||
r.note_event(evt)
|
||||
|
||||
progress = r.render_progress(3.0)
|
||||
assert progress.startswith("working · 3s · item 3")
|
||||
assert "1. ✓ `bash -lc ls`" in progress
|
||||
|
||||
final = r.render_final(3.0, "answer", status="done")
|
||||
assert final.startswith("done · 3s · item 3")
|
||||
assert "running:" not in final
|
||||
assert "ran:" not in final
|
||||
assert final.endswith("answer")
|
||||
|
||||
|
||||
def test_progress_renderer_clamps_actions_and_ignores_unknown() -> None:
|
||||
r = ExecProgressRenderer(max_actions=3, command_width=20)
|
||||
events = [
|
||||
{
|
||||
"type": "item.completed",
|
||||
"item": {
|
||||
"id": f"item_{i}",
|
||||
"type": "command_execution",
|
||||
"command": f"echo {i}",
|
||||
"exit_code": 0,
|
||||
"status": "completed",
|
||||
},
|
||||
}
|
||||
for i in range(6)
|
||||
]
|
||||
|
||||
for evt in events:
|
||||
assert r.note_event(evt) is True
|
||||
|
||||
assert len(r.recent_actions) == 3
|
||||
assert r.recent_actions[0].startswith("3. ")
|
||||
assert r.recent_actions[-1].startswith("5. ")
|
||||
assert r.note_event({"type": "mystery"}) is False
|
||||
@@ -0,0 +1,31 @@
|
||||
import asyncio
|
||||
|
||||
from takopi.exec_bridge import CodexExecRunner
|
||||
|
||||
|
||||
def test_run_serialized_serializes_same_session() -> None:
|
||||
runner = CodexExecRunner(codex_cmd="codex", workspace=None, extra_args=[])
|
||||
gate = asyncio.Event()
|
||||
in_flight = 0
|
||||
max_in_flight = 0
|
||||
|
||||
async def run_stub(*_args, **_kwargs):
|
||||
nonlocal in_flight, max_in_flight
|
||||
in_flight += 1
|
||||
max_in_flight = max(max_in_flight, in_flight)
|
||||
await gate.wait()
|
||||
in_flight -= 1
|
||||
return ("sid", "ok", True)
|
||||
|
||||
runner.run = run_stub # type: ignore[assignment]
|
||||
|
||||
async def run_test() -> None:
|
||||
t1 = asyncio.create_task(runner.run_serialized("a", "sid"))
|
||||
t2 = asyncio.create_task(runner.run_serialized("b", "sid"))
|
||||
await asyncio.sleep(0)
|
||||
gate.set()
|
||||
await asyncio.gather(t1, t2)
|
||||
|
||||
asyncio.run(run_test())
|
||||
|
||||
assert max_in_flight == 1
|
||||
@@ -0,0 +1,22 @@
|
||||
from takopi.rendering import render_markdown
|
||||
|
||||
|
||||
def test_render_markdown_basic_entities() -> None:
|
||||
text, entities = render_markdown("**bold** and `code`")
|
||||
|
||||
assert text == "bold and code\n\n"
|
||||
assert entities == [
|
||||
{"type": "bold", "offset": 0, "length": 4},
|
||||
{"type": "code", "offset": 9, "length": 4},
|
||||
]
|
||||
|
||||
|
||||
def test_render_markdown_code_fence_language_is_string() -> None:
|
||||
text, entities = render_markdown("```py\nprint('x')\n```")
|
||||
|
||||
assert text == "print('x')\n\n"
|
||||
assert entities is not None
|
||||
assert any(
|
||||
e.get("type") == "pre" and e.get("language") == "py" for e in entities
|
||||
)
|
||||
assert any(e.get("type") == "code" for e in entities)
|
||||
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from takopi import exec_bridge
|
||||
|
||||
|
||||
def test_manage_subprocess_kills_when_terminate_times_out(monkeypatch) -> None:
|
||||
async def fake_wait_for(awaitable, *args, **kwargs):
|
||||
if hasattr(awaitable, "close"):
|
||||
awaitable.close()
|
||||
elif hasattr(awaitable, "cancel"):
|
||||
awaitable.cancel()
|
||||
raise asyncio.TimeoutError
|
||||
|
||||
monkeypatch.setattr(exec_bridge.asyncio, "wait_for", fake_wait_for)
|
||||
|
||||
async def run() -> int | None:
|
||||
async with exec_bridge.manage_subprocess(
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import signal, time; signal.signal(signal.SIGTERM, signal.SIG_IGN); time.sleep(10)",
|
||||
) as proc:
|
||||
assert proc.returncode is None
|
||||
return proc.returncode
|
||||
|
||||
rc = asyncio.run(run())
|
||||
|
||||
assert rc is not None
|
||||
assert rc != 0
|
||||
@@ -0,0 +1,79 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from takopi.logging import RedactTokenFilter
|
||||
from takopi.telegram_client import TelegramClient
|
||||
|
||||
|
||||
def test_telegram_429_retry_after_calls_sleep() -> None:
|
||||
calls: list[int] = []
|
||||
sleeps: list[float] = []
|
||||
|
||||
async def fake_sleep(seconds: float) -> None:
|
||||
sleeps.append(seconds)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
calls.append(1)
|
||||
if len(calls) == 1:
|
||||
return httpx.Response(
|
||||
429,
|
||||
json={
|
||||
"ok": False,
|
||||
"description": "retry",
|
||||
"parameters": {"retry_after": 3},
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={"ok": True, "result": {"message_id": 1}},
|
||||
request=request,
|
||||
)
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
|
||||
async def run() -> dict:
|
||||
client = httpx.AsyncClient(transport=transport)
|
||||
try:
|
||||
tg = TelegramClient("123:abcDEF_ghij", client=client, sleep=fake_sleep)
|
||||
return await tg._post("sendMessage", {"chat_id": 1, "text": "hi"})
|
||||
finally:
|
||||
await client.aclose()
|
||||
|
||||
result = asyncio.run(run())
|
||||
|
||||
assert result == {"message_id": 1}
|
||||
assert sleeps == [3]
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
def test_no_token_in_logs_on_http_error(caplog: pytest.LogCaptureFixture) -> None:
|
||||
token = "123:abcDEF_ghij"
|
||||
redactor = RedactTokenFilter()
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.addFilter(redactor)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(500, text="oops", request=request)
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
|
||||
async def run() -> None:
|
||||
client = httpx.AsyncClient(transport=transport)
|
||||
try:
|
||||
tg = TelegramClient(token, client=client)
|
||||
await tg._post("getUpdates", {"timeout": 1})
|
||||
finally:
|
||||
await client.aclose()
|
||||
|
||||
caplog.set_level(logging.ERROR)
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
asyncio.run(run())
|
||||
|
||||
root_logger.removeFilter(redactor)
|
||||
|
||||
assert token not in caplog.text
|
||||
assert "bot[REDACTED]" in caplog.text
|
||||
Reference in New Issue
Block a user