fix(telegram,core): first message seeds the branch, registered commands, no ellipses, html drafts
This commit is contained in:
@@ -535,7 +535,7 @@ class Conversations:
|
|||||||
await self._queue.push(
|
await self._queue.push(
|
||||||
conversation_id=cast("int", conv.id),
|
conversation_id=cast("int", conv.id),
|
||||||
priority="user",
|
priority="user",
|
||||||
origin=f"сид:{seed}",
|
origin=origin if text and seed != "brief" else f"сид:{seed}",
|
||||||
text=prompt,
|
text=prompt,
|
||||||
)
|
)
|
||||||
self._ensure_worker(cast("int", conv.id))
|
self._ensure_worker(cast("int", conv.id))
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import time
|
|||||||
import zlib
|
import zlib
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from aiogram.exceptions import TelegramAPIError
|
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest
|
||||||
|
|
||||||
|
from beaver_gateway.frontends.telegram.render import to_html
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
@@ -37,7 +39,7 @@ class Draft:
|
|||||||
thread_id: int | None,
|
thread_id: int | None,
|
||||||
turn_id: str,
|
turn_id: str,
|
||||||
interval: float = 0.7,
|
interval: float = 0.7,
|
||||||
status: str = "⏳ думаю…",
|
status: str = "⏳ думаю",
|
||||||
) -> None:
|
) -> None:
|
||||||
self._bot = bot
|
self._bot = bot
|
||||||
self._chat_id = chat_id
|
self._chat_id = chat_id
|
||||||
@@ -82,20 +84,27 @@ class Draft:
|
|||||||
async def _push(self) -> None:
|
async def _push(self) -> None:
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
self._last_sent = time.monotonic()
|
self._last_sent = time.monotonic()
|
||||||
|
text = self._render()
|
||||||
try:
|
try:
|
||||||
await self._bot.send_message_draft(
|
try:
|
||||||
chat_id=self._chat_id,
|
await self._send(to_html(text), "HTML")
|
||||||
draft_id=self._draft_id,
|
except TelegramBadRequest:
|
||||||
message_thread_id=self._thread_id,
|
await self._send(text, None)
|
||||||
text=self._render(),
|
|
||||||
parse_mode=None,
|
|
||||||
)
|
|
||||||
except TelegramAPIError as exc:
|
except TelegramAPIError as exc:
|
||||||
self._broken = True
|
self._broken = True
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"draft to %s/%s stopped: %s", self._chat_id, self._thread_id, exc
|
"draft to %s/%s stopped: %s", self._chat_id, self._thread_id, exc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _send(self, text: str, parse_mode: str | None) -> None:
|
||||||
|
await self._bot.send_message_draft(
|
||||||
|
chat_id=self._chat_id,
|
||||||
|
draft_id=self._draft_id,
|
||||||
|
message_thread_id=self._thread_id,
|
||||||
|
text=text,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
)
|
||||||
|
|
||||||
def _render(self) -> str:
|
def _render(self) -> str:
|
||||||
tail = self.text[-_TAIL:]
|
tail = self.text[-_TAIL:]
|
||||||
return f"{self.status}\n\n{tail}" if tail.strip() else self.status
|
return f"{self.status}\n\n{tail}" if tail.strip() else self.status
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from aiogram import Bot
|
|||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
from aiogram.exceptions import TelegramAPIError
|
from aiogram.exceptions import TelegramAPIError
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
|
BotCommand,
|
||||||
CallbackQuery,
|
CallbackQuery,
|
||||||
InlineKeyboardButton,
|
InlineKeyboardButton,
|
||||||
InlineKeyboardMarkup,
|
InlineKeyboardMarkup,
|
||||||
@@ -53,13 +54,16 @@ _log = logging.getLogger("beaver_gateway.frontends.telegram")
|
|||||||
|
|
||||||
FRONTEND = "telegram"
|
FRONTEND = "telegram"
|
||||||
_DONE = "done"
|
_DONE = "done"
|
||||||
_COMMANDS = ("merge", "new", "chat", "status", "start", "help")
|
_COMMAND_HELP = (
|
||||||
_HELP = (
|
("merge", "слить ветку в мастер"),
|
||||||
"General - мастер, топик - ветка. Создай топик и пиши в него.\n"
|
("new", "новая ветка: в General - новый топик, в топике - заново на нём"),
|
||||||
"/merge - слить ветку в мастер\n"
|
("chat", "открыть глубокий чат в vault: /chat тема"),
|
||||||
"/new [название] - новая ветка (в General - новый топик)\n"
|
("status", "что с этим разговором"), # noqa: RUF001
|
||||||
"/chat <тема> - открыть глубокий чат в vault\n"
|
("help", "команды"),
|
||||||
"/status - что с этим разговором" # noqa: RUF001
|
)
|
||||||
|
_COMMANDS = (*(c for c, _ in _COMMAND_HELP), "start")
|
||||||
|
_HELP = "General - мастер, любой другой топик - ветка.\n" + "\n".join(
|
||||||
|
f"/{c} - {d}" for c, d in _COMMAND_HELP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -171,6 +175,9 @@ class TelegramFrontend(Frontend):
|
|||||||
getattr(me, "has_topics_enabled", None),
|
getattr(me, "has_topics_enabled", None),
|
||||||
)
|
)
|
||||||
self._sweep_attachments()
|
self._sweep_attachments()
|
||||||
|
await self.bot.set_my_commands(
|
||||||
|
[BotCommand(command=c, description=d) for c, d in _COMMAND_HELP]
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
async with asyncio.TaskGroup() as tg:
|
async with asyncio.TaskGroup() as tg:
|
||||||
tg.create_task(self.inbox.run())
|
tg.create_task(self.inbox.run())
|
||||||
@@ -305,25 +312,35 @@ class TelegramFrontend(Frontend):
|
|||||||
self._master_window = self._ext(topic.message_thread_id)
|
self._master_window = self._ext(topic.message_thread_id)
|
||||||
return self._master_window
|
return self._master_window
|
||||||
|
|
||||||
async def _master(self) -> Conversation:
|
async def _master(self, text: str | None = None) -> tuple[Conversation, bool]:
|
||||||
|
"""The open master behind its window, spawning one when there is none.
|
||||||
|
|
||||||
|
``text`` rides with the seed of a fresh master; the second value says
|
||||||
|
whether it was consumed that way.
|
||||||
|
"""
|
||||||
ext = await self._master_ext()
|
ext = await self._master_ext()
|
||||||
conv = await self.conversations.find_bound(frontend=FRONTEND, external_id=ext)
|
conv = await self.conversations.find_bound(frontend=FRONTEND, external_id=ext)
|
||||||
if conv is not None and conv.status == "open":
|
if conv is not None and conv.status == "open":
|
||||||
return conv
|
return conv, False
|
||||||
masters = await self.conversations.find(kind="master", status="open", limit=1)
|
masters = await self.conversations.find(kind="master", status="open", limit=1)
|
||||||
if masters:
|
if masters:
|
||||||
await self.conversations.bind(
|
await self.conversations.bind(
|
||||||
masters[0], frontend=FRONTEND, external_id=ext
|
masters[0], frontend=FRONTEND, external_id=ext
|
||||||
)
|
)
|
||||||
return masters[0]
|
return masters[0], False
|
||||||
return await self.conversations.spawn(
|
conv = await self.conversations.spawn(
|
||||||
kind="master", seed="clean", origin=FRONTEND, binding=(FRONTEND, ext)
|
kind="master",
|
||||||
|
seed="clean",
|
||||||
|
text=text,
|
||||||
|
origin=FRONTEND,
|
||||||
|
binding=(FRONTEND, ext),
|
||||||
)
|
)
|
||||||
|
return conv, text is not None
|
||||||
|
|
||||||
async def _branch(
|
async def _branch(
|
||||||
self, thread_id: int, *, title: str | None, text: str | None
|
self, thread_id: int, *, title: str | None, text: str | None
|
||||||
) -> Conversation:
|
) -> Conversation:
|
||||||
master = await self._master()
|
master, _ = await self._master()
|
||||||
return await self.conversations.spawn(
|
return await self.conversations.spawn(
|
||||||
kind="branch",
|
kind="branch",
|
||||||
seed="morning",
|
seed="morning",
|
||||||
@@ -343,10 +360,13 @@ class TelegramFrontend(Frontend):
|
|||||||
await self._on_callback(update.callback_query)
|
await self._on_callback(update.callback_query)
|
||||||
|
|
||||||
async def _on_message(self, message: Message) -> None:
|
async def _on_message(self, message: Message) -> None:
|
||||||
if message.forum_topic_created is not None and message.message_thread_id:
|
created = message.forum_topic_created
|
||||||
self._topic_names[message.message_thread_id] = (
|
if (
|
||||||
message.forum_topic_created.name
|
created is not None
|
||||||
)
|
and message.message_thread_id
|
||||||
|
and not created.is_name_implicit
|
||||||
|
):
|
||||||
|
self._topic_names[message.message_thread_id] = created.name
|
||||||
if message.from_user is None or message.from_user.id != self.user_id:
|
if message.from_user is None or message.from_user.id != self.user_id:
|
||||||
_log.warning(
|
_log.warning(
|
||||||
"ignoring message from %s", getattr(message.from_user, "id", None)
|
"ignoring message from %s", getattr(message.from_user, "id", None)
|
||||||
@@ -359,11 +379,7 @@ class TelegramFrontend(Frontend):
|
|||||||
is_master = (
|
is_master = (
|
||||||
thread_id is None or self._ext(thread_id) == await self._master_ext()
|
thread_id is None or self._ext(thread_id) == await self._master_ext()
|
||||||
)
|
)
|
||||||
if message.forum_topic_created is not None and thread_id is not None:
|
if message.forum_topic_created is not None:
|
||||||
if not is_master and await self._live(thread_id) is None:
|
|
||||||
await self._branch(
|
|
||||||
thread_id, title=message.forum_topic_created.name, text=None
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
if message.forum_topic_edited is not None and thread_id is not None:
|
if message.forum_topic_edited is not None and thread_id is not None:
|
||||||
if message.forum_topic_edited.name:
|
if message.forum_topic_edited.name:
|
||||||
@@ -382,11 +398,14 @@ class TelegramFrontend(Frontend):
|
|||||||
await self._command(command, args.strip(), message, thread_id)
|
await self._command(command, args.strip(), message, thread_id)
|
||||||
return
|
return
|
||||||
if is_master:
|
if is_master:
|
||||||
conv = await self._master()
|
conv, consumed = await self._master(text)
|
||||||
|
if consumed:
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
conv = await self._live(thread_id)
|
conv = await self._live(thread_id)
|
||||||
if conv is None:
|
if conv is None:
|
||||||
await self._branch(thread_id, title=self._title_from(text), text=text)
|
title = self._topic_names.get(thread_id) or self._title_from(text)
|
||||||
|
await self._branch(thread_id, title=title, text=text)
|
||||||
return
|
return
|
||||||
pending = self.conversations.pending_question(conv.external_id)
|
pending = self.conversations.pending_question(conv.external_id)
|
||||||
if pending is not None and self.conversations.answer(pending[0], text):
|
if pending is not None and self.conversations.answer(pending[0], text):
|
||||||
@@ -522,14 +541,14 @@ class TelegramFrontend(Frontend):
|
|||||||
is_master = (
|
is_master = (
|
||||||
thread_id is None or self._ext(thread_id) == await self._master_ext()
|
thread_id is None or self._ext(thread_id) == await self._master_ext()
|
||||||
)
|
)
|
||||||
conv = await self._master() if is_master else await self._live(thread_id)
|
conv = (await self._master())[0] if is_master else await self._live(thread_id)
|
||||||
if command == "status":
|
if command == "status":
|
||||||
return await self._status(conv)
|
return await self._status(conv)
|
||||||
if command == "merge":
|
if command == "merge":
|
||||||
if conv is None or conv.kind != "branch":
|
if conv is None or conv.kind != "branch":
|
||||||
return "сливать нечего: это не открытая ветка"
|
return "сливать нечего: это не открытая ветка"
|
||||||
self._spawn_task(self._merge(conv))
|
self._spawn_task(self._merge(conv))
|
||||||
return "🔀 сливаю в мастер…"
|
return "🔀 сливаю в мастер"
|
||||||
if command == "new":
|
if command == "new":
|
||||||
if is_master or thread_id is None:
|
if is_master or thread_id is None:
|
||||||
child = await self.conversations.spawn(
|
child = await self.conversations.spawn(
|
||||||
@@ -649,6 +668,10 @@ class TelegramFrontend(Frontend):
|
|||||||
key=f"{event.get('turn_id')}:error",
|
key=f"{event.get('turn_id')}:error",
|
||||||
)
|
)
|
||||||
case "reply":
|
case "reply":
|
||||||
|
if conv.kind == "master" and str(
|
||||||
|
event.get("item_origin") or ""
|
||||||
|
).startswith("сид"):
|
||||||
|
return
|
||||||
await self._on_reply(conv, event)
|
await self._on_reply(conv, event)
|
||||||
case "say":
|
case "say":
|
||||||
await self._deliver(
|
await self._deliver(
|
||||||
@@ -711,10 +734,10 @@ class TelegramFrontend(Frontend):
|
|||||||
if kind == "content_block_delta":
|
if kind == "content_block_delta":
|
||||||
delta = raw.get("delta") or {}
|
delta = raw.get("delta") or {}
|
||||||
if delta.get("type") == "text_delta":
|
if delta.get("type") == "text_delta":
|
||||||
draft.set_status("✍️ пишу…")
|
draft.set_status("✍️ пишу")
|
||||||
draft.append(str(delta.get("text") or ""))
|
draft.append(str(delta.get("text") or ""))
|
||||||
elif delta.get("type") == "thinking_delta":
|
elif delta.get("type") == "thinking_delta":
|
||||||
draft.set_status("🤔 думаю…")
|
draft.set_status("🤔 думаю")
|
||||||
elif kind == "content_block_start":
|
elif kind == "content_block_start":
|
||||||
block = raw.get("content_block") or {}
|
block = raw.get("content_block") or {}
|
||||||
if block.get("type") == "tool_use":
|
if block.get("type") == "tool_use":
|
||||||
|
|||||||
@@ -20,25 +20,25 @@ _BULLET = re.compile(r"^(\s*)[-*]\s+", re.MULTILINE)
|
|||||||
_STRIKE = re.compile(r"~~(.+?)~~")
|
_STRIKE = re.compile(r"~~(.+?)~~")
|
||||||
|
|
||||||
_LABELS: dict[str, str] = {
|
_LABELS: dict[str, str] = {
|
||||||
"Read": "читаю vault…",
|
"Read": "читаю vault",
|
||||||
"Glob": "ищу файлы…",
|
"Glob": "ищу файлы",
|
||||||
"Grep": "ищу в vault…",
|
"Grep": "ищу в vault",
|
||||||
"Edit": "правлю файл…",
|
"Edit": "правлю файл",
|
||||||
"Write": "пишу файл…",
|
"Write": "пишу файл",
|
||||||
"MultiEdit": "правлю файлы…",
|
"MultiEdit": "правлю файлы",
|
||||||
"Bash": "выполняю команду…",
|
"Bash": "выполняю команду",
|
||||||
"WebSearch": "ищу в сети…",
|
"WebSearch": "ищу в сети",
|
||||||
"WebFetch": "читаю страницу…",
|
"WebFetch": "читаю страницу",
|
||||||
"Task": "запустил сабагента…",
|
"Task": "запустил сабагента",
|
||||||
"Agent": "запустил сабагента…",
|
"Agent": "запустил сабагента",
|
||||||
"AskUserQuestion": "спрашиваю…",
|
"AskUserQuestion": "спрашиваю",
|
||||||
"TodoWrite": "планирую…",
|
"TodoWrite": "планирую",
|
||||||
"Skill": "открываю скилл…",
|
"Skill": "открываю скилл",
|
||||||
"mcp__gateway__spawn": "открываю разговор…",
|
"mcp__gateway__spawn": "открываю разговор",
|
||||||
"mcp__gateway__read_conversation": "читаю разговор…",
|
"mcp__gateway__read_conversation": "читаю разговор",
|
||||||
"mcp__gateway__say": "говорю…",
|
"mcp__gateway__say": "говорю",
|
||||||
"mcp__gateway__schedule": "ставлю напоминание…",
|
"mcp__gateway__schedule": "ставлю напоминание",
|
||||||
"mcp__gateway__inject": "передаю в другой разговор…",
|
"mcp__gateway__inject": "передаю в другой разговор",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ def status_label(name: str, tool_input: dict[str, Any] | None = None) -> str:
|
|||||||
parts = name.split("__", 2)
|
parts = name.split("__", 2)
|
||||||
server = parts[1]
|
server = parts[1]
|
||||||
tool = parts[2] if len(parts) == 3 else ""
|
tool = parts[2] if len(parts) == 3 else ""
|
||||||
return f"{server}: {tool}…" if tool else f"{server}…"
|
return f"{server}: {tool}" if tool else server
|
||||||
hint = ""
|
hint = ""
|
||||||
if tool_input:
|
if tool_input:
|
||||||
for key in ("description", "command", "file_path", "pattern", "query"):
|
for key in ("description", "command", "file_path", "pattern", "query"):
|
||||||
@@ -108,4 +108,4 @@ def status_label(name: str, tool_input: dict[str, Any] | None = None) -> str:
|
|||||||
if isinstance(value, str) and value.strip():
|
if isinstance(value, str) and value.strip():
|
||||||
hint = value.strip().splitlines()[0][:60]
|
hint = value.strip().splitlines()[0][:60]
|
||||||
break
|
break
|
||||||
return f"{name} {hint}…".strip() if hint else f"{name}…"
|
return f"{name} {hint}" if hint else name
|
||||||
|
|||||||
+20
-19
@@ -234,7 +234,8 @@ async def stack() -> Stack:
|
|||||||
|
|
||||||
async def test_general_is_master_and_reply_has_no_thread(stack: Stack) -> None:
|
async def test_general_is_master_and_reply_has_no_thread(stack: Stack) -> None:
|
||||||
stack.bot.message("hi")
|
stack.bot.message("hi")
|
||||||
reply = await stack.until(lambda: stack.sent_with("ok:hi"), what="reply")
|
reply = await stack.until(lambda: stack.sent_with("hi"), what="reply")
|
||||||
|
assert reply["text"].startswith("ok:[сид: clean] master")
|
||||||
assert reply["thread"] == 901
|
assert reply["thread"] == 901
|
||||||
assert reply["parse_mode"] == "HTML"
|
assert reply["parse_mode"] == "HTML"
|
||||||
assert stack.bot.topics == ["🦫 General"]
|
assert stack.bot.topics == ["🦫 General"]
|
||||||
@@ -244,7 +245,7 @@ async def test_general_is_master_and_reply_has_no_thread(stack: Stack) -> None:
|
|||||||
assert master is not None and master.kind == "master"
|
assert master is not None and master.kind == "master"
|
||||||
assert stack.bot.drafts and stack.bot.drafts[0]["chat_id"] == USER
|
assert stack.bot.drafts and stack.bot.drafts[0]["chat_id"] == USER
|
||||||
rows = await stack.world.conversations.queue.recent(master.id)
|
rows = await stack.world.conversations.queue.recent(master.id)
|
||||||
assert {r.origin for r in rows} == {"сид:clean", "telegram"}
|
assert [r.origin for r in rows] == ["telegram"]
|
||||||
|
|
||||||
|
|
||||||
async def test_new_topic_becomes_morning_branch_and_replies_in_thread(
|
async def test_new_topic_becomes_morning_branch_and_replies_in_thread(
|
||||||
@@ -252,8 +253,9 @@ async def test_new_topic_becomes_morning_branch_and_replies_in_thread(
|
|||||||
) -> None:
|
) -> None:
|
||||||
stack.bot.topic_created("план", 7)
|
stack.bot.topic_created("план", 7)
|
||||||
stack.bot.message("hello topic", thread=7)
|
stack.bot.message("hello topic", thread=7)
|
||||||
reply = await stack.until(lambda: stack.sent_with("ok:hello topic"), what="reply")
|
reply = await stack.until(lambda: stack.sent_with("hello topic"), what="reply")
|
||||||
assert reply["thread"] == 7
|
assert reply["thread"] == 7
|
||||||
|
assert len([m for m in stack.bot.sent if m["thread"] == 7]) == 1
|
||||||
branch = await stack.world.conversations.find_bound(
|
branch = await stack.world.conversations.find_bound(
|
||||||
frontend="telegram", external_id=f"{USER}/7"
|
frontend="telegram", external_id=f"{USER}/7"
|
||||||
)
|
)
|
||||||
@@ -266,6 +268,7 @@ async def test_new_topic_becomes_morning_branch_and_replies_in_thread(
|
|||||||
prompts = [p for c in ScriptedClient.instances for p in c.prompts]
|
prompts = [p for c in ScriptedClient.instances for p in c.prompts]
|
||||||
seed = next(p for p in prompts if p.startswith("[сид: morning] branch «план»"))
|
seed = next(p for p in prompts if p.startswith("[сид: morning] branch «план»"))
|
||||||
assert "Хендаут не приехал." in seed
|
assert "Хендаут не приехал." in seed
|
||||||
|
assert seed.endswith("hello topic")
|
||||||
assert stack.bot.drafts[-1]["message_thread_id"] == 7
|
assert stack.bot.drafts[-1]["message_thread_id"] == 7
|
||||||
|
|
||||||
|
|
||||||
@@ -317,7 +320,7 @@ async def test_message_into_merged_branch_rebinds_the_topic(stack: Stack) -> Non
|
|||||||
|
|
||||||
async def test_reply_from_another_window_is_mirrored_with_marker(stack: Stack) -> None:
|
async def test_reply_from_another_window_is_mirrored_with_marker(stack: Stack) -> None:
|
||||||
stack.bot.message("hi")
|
stack.bot.message("hi")
|
||||||
await stack.until(lambda: stack.sent_with("ok:hi"), what="reply")
|
await stack.until(lambda: stack.sent_with("hi"), what="reply")
|
||||||
master = await stack.world.conversations.find_bound(
|
master = await stack.world.conversations.find_bound(
|
||||||
frontend="telegram", external_id=GENERAL
|
frontend="telegram", external_id=GENERAL
|
||||||
)
|
)
|
||||||
@@ -331,7 +334,7 @@ async def test_reply_from_another_window_is_mirrored_with_marker(stack: Stack) -
|
|||||||
|
|
||||||
async def test_say_is_delivered_and_inject_turns_are_silent(stack: Stack) -> None:
|
async def test_say_is_delivered_and_inject_turns_are_silent(stack: Stack) -> None:
|
||||||
stack.bot.message("hi")
|
stack.bot.message("hi")
|
||||||
await stack.until(lambda: stack.sent_with("ok:hi"), what="reply")
|
await stack.until(lambda: stack.sent_with("hi"), what="reply")
|
||||||
master = await stack.world.conversations.find_bound(
|
master = await stack.world.conversations.find_bound(
|
||||||
frontend="telegram", external_id=GENERAL
|
frontend="telegram", external_id=GENERAL
|
||||||
)
|
)
|
||||||
@@ -339,7 +342,7 @@ async def test_say_is_delivered_and_inject_turns_are_silent(stack: Stack) -> Non
|
|||||||
await stack.world.conversations.inject(
|
await stack.world.conversations.inject(
|
||||||
master, "cron tick", urgency="urgent", origin="крон"
|
master, "cron tick", urgency="urgent", origin="крон"
|
||||||
)
|
)
|
||||||
await stack.world.settle(master, 3)
|
await stack.world.settle(master, 2)
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
assert len(stack.bot.sent) == before
|
assert len(stack.bot.sent) == before
|
||||||
await stack.world.conversations.say(master, "psst")
|
await stack.world.conversations.say(master, "psst")
|
||||||
@@ -348,7 +351,7 @@ async def test_say_is_delivered_and_inject_turns_are_silent(stack: Stack) -> Non
|
|||||||
|
|
||||||
async def test_question_becomes_buttons_and_callback_answers(stack: Stack) -> None:
|
async def test_question_becomes_buttons_and_callback_answers(stack: Stack) -> None:
|
||||||
stack.bot.message("hi")
|
stack.bot.message("hi")
|
||||||
await stack.until(lambda: stack.sent_with("ok:hi"), what="reply")
|
await stack.until(lambda: stack.sent_with("hi"), what="reply")
|
||||||
master = await stack.world.conversations.find_bound(
|
master = await stack.world.conversations.find_bound(
|
||||||
frontend="telegram", external_id=GENERAL
|
frontend="telegram", external_id=GENERAL
|
||||||
)
|
)
|
||||||
@@ -393,7 +396,7 @@ async def test_question_timeout_renders_text_and_free_text_answers(
|
|||||||
stack: Stack,
|
stack: Stack,
|
||||||
) -> None:
|
) -> None:
|
||||||
stack.bot.message("hi")
|
stack.bot.message("hi")
|
||||||
await stack.until(lambda: stack.sent_with("ok:hi"), what="reply")
|
await stack.until(lambda: stack.sent_with("hi"), what="reply")
|
||||||
master = await stack.world.conversations.find_bound(
|
master = await stack.world.conversations.find_bound(
|
||||||
frontend="telegram", external_id=GENERAL
|
frontend="telegram", external_id=GENERAL
|
||||||
)
|
)
|
||||||
@@ -455,16 +458,16 @@ async def test_commands_status_merge_and_new(stack: Stack) -> None:
|
|||||||
|
|
||||||
async def test_master_topic_survives_rotation(stack: Stack) -> None:
|
async def test_master_topic_survives_rotation(stack: Stack) -> None:
|
||||||
stack.bot.message("hi")
|
stack.bot.message("hi")
|
||||||
await stack.until(lambda: stack.sent_with("ok:hi"), what="reply")
|
await stack.until(lambda: stack.sent_with("hi"), what="reply")
|
||||||
master = await stack.world.conversations.find_bound(
|
master = await stack.world.conversations.find_bound(
|
||||||
frontend="telegram", external_id=GENERAL
|
frontend="telegram", external_id=GENERAL
|
||||||
)
|
)
|
||||||
stack.bot.message("into general", thread=901)
|
stack.bot.message("into general", thread=901)
|
||||||
await stack.until(lambda: stack.sent_with("ok:into general"), what="reply")
|
await stack.until(lambda: stack.sent_with("into general"), what="reply")
|
||||||
assert (await stack.world.conversations.find(kind="branch")) == []
|
assert (await stack.world.conversations.find(kind="branch")) == []
|
||||||
await stack.world.conversations.set_status(master, "closed")
|
await stack.world.conversations.set_status(master, "closed")
|
||||||
stack.bot.message("after rotation", thread=901)
|
stack.bot.message("after rotation", thread=901)
|
||||||
await stack.until(lambda: stack.sent_with("ok:after rotation"), what="reply")
|
await stack.until(lambda: stack.sent_with("after rotation"), what="reply")
|
||||||
fresh = await stack.world.conversations.find_bound(
|
fresh = await stack.world.conversations.find_bound(
|
||||||
frontend="telegram", external_id=GENERAL
|
frontend="telegram", external_id=GENERAL
|
||||||
)
|
)
|
||||||
@@ -475,7 +478,7 @@ async def test_master_topic_survives_rotation(stack: Stack) -> None:
|
|||||||
|
|
||||||
async def test_inbox_stores_first_and_replays_after_restart(stack: Stack) -> None:
|
async def test_inbox_stores_first_and_replays_after_restart(stack: Stack) -> None:
|
||||||
stack.bot.message("hi")
|
stack.bot.message("hi")
|
||||||
await stack.until(lambda: stack.sent_with("ok:hi"), what="reply")
|
await stack.until(lambda: stack.sent_with("hi"), what="reply")
|
||||||
async with stack.world.db.session() as session:
|
async with stack.world.db.session() as session:
|
||||||
rows = list((await session.exec(select(TelegramUpdate))).all())
|
rows = list((await session.exec(select(TelegramUpdate))).all())
|
||||||
assert [r.update_id for r in rows] == [1000]
|
assert [r.update_id for r in rows] == [1000]
|
||||||
@@ -498,7 +501,7 @@ async def test_inbox_stores_first_and_replays_after_restart(stack: Stack) -> Non
|
|||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await stack.until(
|
await stack.until(
|
||||||
lambda: stack.sent_with("ok:replayed"), timeout=8, what="replayed reply"
|
lambda: stack.sent_with("replayed"), timeout=8, what="replayed reply"
|
||||||
)
|
)
|
||||||
stack.bot.message("stranger", uid=1)
|
stack.bot.message("stranger", uid=1)
|
||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
@@ -509,7 +512,7 @@ async def test_outbox_retries_and_falls_back_to_plain(stack: Stack) -> None:
|
|||||||
stack.bot.fail_sends = 2
|
stack.bot.fail_sends = 2
|
||||||
stack.bot.reject_html = True
|
stack.bot.reject_html = True
|
||||||
stack.bot.message("**bold**")
|
stack.bot.message("**bold**")
|
||||||
reply = await stack.until(lambda: stack.sent_with("ok:**bold**"), what="reply")
|
reply = await stack.until(lambda: stack.sent_with("**bold**"), what="reply")
|
||||||
assert reply["parse_mode"] is None
|
assert reply["parse_mode"] is None
|
||||||
async with stack.world.db.session() as session:
|
async with stack.world.db.session() as session:
|
||||||
rows = list((await session.exec(select(Delivery))).all())
|
rows = list((await session.exec(select(Delivery))).all())
|
||||||
@@ -548,8 +551,6 @@ def test_render_helpers() -> None:
|
|||||||
)
|
)
|
||||||
parts = chunks("абв\n\n" + "г" * 30 + "\n\n" + "д" * 30, limit=40)
|
parts = chunks("абв\n\n" + "г" * 30 + "\n\n" + "д" * 30, limit=40)
|
||||||
assert parts == ["абв\n\n" + "г" * 30, "д" * 30]
|
assert parts == ["абв\n\n" + "г" * 30, "д" * 30]
|
||||||
assert status_label("Read", {"file_path": "/x"}) == "читаю vault…"
|
assert status_label("Read", {"file_path": "/x"}) == "читаю vault"
|
||||||
assert (
|
assert status_label("mcp__firefly__list_accounts", None) == "firefly: list_accounts"
|
||||||
status_label("mcp__firefly__list_accounts", None) == "firefly: list_accounts…"
|
assert status_label("Foo", {"command": "ls -la"}) == "Foo ls -la"
|
||||||
)
|
|
||||||
assert status_label("Foo", {"command": "ls -la"}) == "Foo ls -la…"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user