refactor: simplify telegram markdown rendering (#20)
This commit is contained in:
+27
-27
@@ -4,8 +4,8 @@ import anyio
|
||||
import pytest
|
||||
|
||||
from takopi.bridge import _build_bot_commands, _strip_engine_command
|
||||
from takopi.markdown import prepare_telegram, truncate_for_telegram
|
||||
from takopi.model import EngineId, ResumeToken, TakopiEvent
|
||||
from takopi.render import MarkdownParts, prepare_telegram
|
||||
from takopi.router import AutoRouter, RunnerEntry
|
||||
from takopi.runners.codex import CodexRunner
|
||||
from takopi.runners.mock import Advance, Emit, Raise, Return, ScriptRunner, Sleep, Wait
|
||||
@@ -101,25 +101,33 @@ def test_codex_extract_resume_accepts_uuid7() -> None:
|
||||
assert runner.extract_resume(text) == ResumeToken(engine=CODEX_ENGINE, value=token)
|
||||
|
||||
|
||||
def test_truncate_for_telegram_preserves_resume_line() -> None:
|
||||
uuid = "019b66fc-64c2-7a71-81cd-081c504cfeb2"
|
||||
md = ("x" * 10_000) + f"\n`codex resume {uuid}`"
|
||||
def test_prepare_telegram_trims_body_preserves_footer() -> None:
|
||||
body_limit = 3500
|
||||
parts = MarkdownParts(
|
||||
header="header",
|
||||
body="x" * (body_limit + 100),
|
||||
footer="footer",
|
||||
)
|
||||
|
||||
runner = CodexRunner(codex_cmd="codex", extra_args=[])
|
||||
out = truncate_for_telegram(md, 400, is_resume_line=runner.is_resume_line)
|
||||
rendered, _ = prepare_telegram(parts)
|
||||
|
||||
assert len(out) <= 400
|
||||
assert f"codex resume {uuid}" in out
|
||||
assert out.rstrip().endswith(f"`codex resume {uuid}`")
|
||||
chunks = [chunk for chunk in rendered.split("\n\n") if chunk]
|
||||
assert chunks[0] == "header"
|
||||
assert chunks[-1].rstrip() == "footer"
|
||||
assert len(chunks[1]) == body_limit
|
||||
assert chunks[1].endswith("…")
|
||||
|
||||
|
||||
def test_truncate_for_telegram_keeps_last_non_empty_line() -> None:
|
||||
md = "intro\n\n" + ("x" * 500) + "\nlast line"
|
||||
def test_prepare_telegram_preserves_entities_on_truncate() -> None:
|
||||
body_limit = 3500
|
||||
parts = MarkdownParts(
|
||||
header="h",
|
||||
body="**bold** " + ("x" * (body_limit + 100)),
|
||||
)
|
||||
|
||||
out = truncate_for_telegram(md, 120, is_resume_line=lambda _line: False)
|
||||
_, entities = prepare_telegram(parts)
|
||||
|
||||
assert len(out) <= 120
|
||||
assert out.rstrip().endswith("last line")
|
||||
assert any(e.get("type") == "bold" for e in entities)
|
||||
|
||||
|
||||
def test_strip_engine_command_inline() -> None:
|
||||
@@ -171,15 +179,6 @@ def test_build_bot_commands_includes_cancel_and_engine() -> None:
|
||||
assert any(cmd["command"] == "codex" for cmd in commands)
|
||||
|
||||
|
||||
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
|
||||
@@ -364,7 +363,7 @@ async def test_handle_message_strips_resume_line_from_prompt() -> None:
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_new_final_message_forces_notification_when_too_long_to_edit() -> None:
|
||||
async def test_long_final_message_edits_progress_message() -> None:
|
||||
from takopi.bridge import BridgeConfig, handle_message
|
||||
|
||||
bot = _FakeBot()
|
||||
@@ -386,9 +385,9 @@ async def test_new_final_message_forces_notification_when_too_long_to_edit() ->
|
||||
resume_token=None,
|
||||
)
|
||||
|
||||
assert len(bot.send_calls) == 2
|
||||
assert len(bot.send_calls) == 1
|
||||
assert bot.send_calls[0]["disable_notification"] is True
|
||||
assert bot.send_calls[1]["disable_notification"] is False
|
||||
assert len(bot.edit_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -553,7 +552,8 @@ async def test_bridge_flow_sends_progress_edits_and_final_resume() -> None:
|
||||
)
|
||||
|
||||
assert bot.send_calls[0]["reply_to_message_id"] == 42
|
||||
assert "working" in bot.send_calls[0]["text"]
|
||||
assert "starting" in bot.send_calls[0]["text"]
|
||||
assert "codex" in bot.send_calls[0]["text"]
|
||||
assert len(bot.edit_calls) >= 1
|
||||
assert session_id in bot.send_calls[-1]["text"]
|
||||
assert "codex resume" in bot.send_calls[-1]["text"].lower()
|
||||
|
||||
+105
-16
@@ -2,9 +2,18 @@ from typing import cast
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
|
||||
from takopi.markdown import render_markdown
|
||||
from takopi.model import TakopiEvent
|
||||
from takopi.render import ExecProgressRenderer, render_event_cli
|
||||
from takopi.model import Action, ActionEvent, ResumeToken, StartedEvent, TakopiEvent
|
||||
from takopi.render import (
|
||||
ExecProgressRenderer,
|
||||
STATUS,
|
||||
action_status,
|
||||
assemble_markdown_parts,
|
||||
format_elapsed,
|
||||
format_file_change_title,
|
||||
render_event_cli,
|
||||
render_markdown,
|
||||
shorten,
|
||||
)
|
||||
from tests.factories import (
|
||||
action_completed,
|
||||
action_started,
|
||||
@@ -109,17 +118,21 @@ def test_file_change_renders_relative_paths_inside_cwd() -> None:
|
||||
|
||||
|
||||
def test_progress_renderer_renders_progress_and_final() -> None:
|
||||
r = ExecProgressRenderer(max_actions=5, resume_formatter=_format_resume)
|
||||
r = ExecProgressRenderer(
|
||||
max_actions=5, resume_formatter=_format_resume, engine="codex"
|
||||
)
|
||||
for evt in SAMPLE_EVENTS:
|
||||
r.note_event(evt)
|
||||
|
||||
progress = r.render_progress(3.0)
|
||||
assert progress.startswith("working · 3s · step 2")
|
||||
progress_parts = r.render_progress_parts(3.0)
|
||||
progress = assemble_markdown_parts(progress_parts)
|
||||
assert progress.startswith("working · codex · 3s · step 2")
|
||||
assert "✓ `bash -lc ls`" in progress
|
||||
assert "`codex resume 0199a213-81c0-7800-8aa1-bbab2a035a53`" in progress
|
||||
|
||||
final = r.render_final(3.0, "answer", status="done")
|
||||
assert final.startswith("done · 3s · step 2")
|
||||
final_parts = r.render_final_parts(3.0, "answer", status="done")
|
||||
final = assemble_markdown_parts(final_parts)
|
||||
assert final.startswith("done · codex · 3s · step 2")
|
||||
assert "answer" in final
|
||||
assert final.rstrip().endswith(
|
||||
"`codex resume 0199a213-81c0-7800-8aa1-bbab2a035a53`"
|
||||
@@ -127,7 +140,7 @@ def test_progress_renderer_renders_progress_and_final() -> None:
|
||||
|
||||
|
||||
def test_progress_renderer_clamps_actions_and_ignores_unknown() -> None:
|
||||
r = ExecProgressRenderer(max_actions=3, command_width=20)
|
||||
r = ExecProgressRenderer(max_actions=3, command_width=20, engine="codex")
|
||||
events = [
|
||||
action_completed(
|
||||
f"item_{i}",
|
||||
@@ -150,7 +163,7 @@ def test_progress_renderer_clamps_actions_and_ignores_unknown() -> None:
|
||||
|
||||
|
||||
def test_progress_renderer_renders_commands_in_markdown() -> None:
|
||||
r = ExecProgressRenderer(max_actions=5, command_width=None)
|
||||
r = ExecProgressRenderer(max_actions=5, command_width=None, engine="codex")
|
||||
for i in (30, 31, 32):
|
||||
r.note_event(
|
||||
action_completed(
|
||||
@@ -162,7 +175,7 @@ def test_progress_renderer_renders_commands_in_markdown() -> None:
|
||||
)
|
||||
)
|
||||
|
||||
md = r.render_progress(0.0)
|
||||
md = assemble_markdown_parts(r.render_progress_parts(0.0))
|
||||
text, _ = render_markdown(md)
|
||||
assert "✓ echo 30" in text
|
||||
assert "✓ echo 31" in text
|
||||
@@ -170,7 +183,7 @@ def test_progress_renderer_renders_commands_in_markdown() -> None:
|
||||
|
||||
|
||||
def test_progress_renderer_handles_duplicate_action_ids() -> None:
|
||||
r = ExecProgressRenderer(max_actions=5)
|
||||
r = ExecProgressRenderer(max_actions=5, engine="codex")
|
||||
events = [
|
||||
action_started("dup", "command", "echo first"),
|
||||
action_completed(
|
||||
@@ -201,7 +214,7 @@ def test_progress_renderer_handles_duplicate_action_ids() -> None:
|
||||
|
||||
|
||||
def test_progress_renderer_collapses_action_updates() -> None:
|
||||
r = ExecProgressRenderer(max_actions=5)
|
||||
r = ExecProgressRenderer(max_actions=5, engine="codex")
|
||||
events = [
|
||||
action_started("a-1", "command", "echo one"),
|
||||
action_started("a-1", "command", "echo two"),
|
||||
@@ -234,11 +247,87 @@ def test_progress_renderer_deterministic_output() -> None:
|
||||
detail={"exit_code": 0},
|
||||
),
|
||||
]
|
||||
r1 = ExecProgressRenderer(max_actions=5)
|
||||
r2 = ExecProgressRenderer(max_actions=5)
|
||||
r1 = ExecProgressRenderer(max_actions=5, engine="codex")
|
||||
r2 = ExecProgressRenderer(max_actions=5, engine="codex")
|
||||
|
||||
for evt in events:
|
||||
r1.note_event(evt)
|
||||
r2.note_event(evt)
|
||||
|
||||
assert r1.render_progress(1.0) == r2.render_progress(1.0)
|
||||
assert assemble_markdown_parts(
|
||||
r1.render_progress_parts(1.0)
|
||||
) == assemble_markdown_parts(r2.render_progress_parts(1.0))
|
||||
|
||||
|
||||
def test_format_elapsed_branches() -> None:
|
||||
assert format_elapsed(3661) == "1h 01m"
|
||||
assert format_elapsed(61) == "1m 01s"
|
||||
assert format_elapsed(1.4) == "1s"
|
||||
|
||||
|
||||
def test_shorten_and_action_status_branches() -> None:
|
||||
assert shorten("hello", None) == "hello"
|
||||
assert shorten("hello", 0) == ""
|
||||
shortened = shorten("hello world", 6)
|
||||
assert shortened.endswith("…")
|
||||
assert len(shortened) <= 6
|
||||
|
||||
action_ok = Action(id="ok", kind="command", title="x", detail={"exit_code": 0})
|
||||
action_fail = Action(id="fail", kind="command", title="x", detail={"exit_code": 2})
|
||||
|
||||
assert action_status(action_ok, completed=False, ok=None) == STATUS["running"]
|
||||
assert action_status(action_ok, completed=True, ok=None) == STATUS["done"]
|
||||
assert action_status(action_fail, completed=True, ok=None) == STATUS["fail"]
|
||||
|
||||
|
||||
def test_format_file_change_title_handles_overflow_and_invalid() -> None:
|
||||
action = Action(
|
||||
id="f",
|
||||
kind="file_change",
|
||||
title="files",
|
||||
detail={
|
||||
"changes": [
|
||||
"bad",
|
||||
{"path": ""},
|
||||
{"path": "a", "kind": "add"},
|
||||
{"path": "b"},
|
||||
{"path": "c"},
|
||||
{"path": "d"},
|
||||
]
|
||||
},
|
||||
)
|
||||
title = format_file_change_title(action, command_width=200)
|
||||
assert title.startswith("files: ")
|
||||
assert "…(" in title
|
||||
|
||||
fallback = format_file_change_title(
|
||||
Action(id="empty", kind="file_change", title="all files"), command_width=50
|
||||
)
|
||||
assert fallback == "files: all files"
|
||||
|
||||
|
||||
def test_render_event_cli_ignores_turn_actions() -> None:
|
||||
event = ActionEvent(
|
||||
engine="codex",
|
||||
action=Action(id="turn", kind="turn", title="turn"),
|
||||
phase="started",
|
||||
ok=None,
|
||||
)
|
||||
assert render_event_cli(event) == []
|
||||
|
||||
|
||||
def test_progress_renderer_ignores_missing_action_id_and_titles() -> None:
|
||||
renderer = ExecProgressRenderer(engine="codex", show_title=True)
|
||||
resume = ResumeToken(engine="codex", value="abc")
|
||||
renderer.note_event(StartedEvent(engine="codex", resume=resume, title="Session"))
|
||||
|
||||
event = ActionEvent(
|
||||
engine="codex",
|
||||
action=Action(id="", kind="command", title="echo"),
|
||||
phase="started",
|
||||
ok=None,
|
||||
)
|
||||
assert renderer.note_event(event) is False
|
||||
|
||||
header = assemble_markdown_parts(renderer.render_progress_parts(0.0))
|
||||
assert header.startswith("working (Session) · codex · 0s")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from takopi.markdown import render_markdown
|
||||
from takopi.render import render_markdown
|
||||
|
||||
|
||||
def test_render_markdown_basic_entities() -> None:
|
||||
|
||||
Reference in New Issue
Block a user