refactor: simplify runtime, config, and telegram (#85)

This commit is contained in:
banteg
2026-01-11 14:48:39 +04:00
committed by GitHub
parent 2380b3e5e9
commit 194cc02bba
42 changed files with 3204 additions and 3717 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ def test_chat_id_command_uses_config_token(monkeypatch) -> None:
settings = TakopiSettings.model_validate(
{
"transport": "telegram",
"transports": {"telegram": {"bot_token": "config-token"}},
"transports": {"telegram": {"bot_token": "config-token", "chat_id": 123}},
}
)
monkeypatch.setattr(cli, "_load_settings_optional", lambda: (settings, Path("x")))
+10 -11
View File
@@ -4,38 +4,37 @@ from pathlib import Path
import pytest
from takopi.config import ConfigError
from takopi.config_store import read_raw_toml, write_raw_toml
from takopi.config import ConfigError, read_config, write_config
def test_read_write_raw_toml_round_trip(tmp_path: Path) -> None:
def test_read_write_config_round_trip(tmp_path: Path) -> None:
config_path = tmp_path / "takopi.toml"
payload = {
"default_engine": "codex",
"projects": {"z80": {"path": "/tmp/repo"}},
}
write_raw_toml(payload, config_path)
loaded = read_raw_toml(config_path)
write_config(payload, config_path)
loaded = read_config(config_path)
assert loaded == payload
def test_read_raw_toml_missing_file(tmp_path: Path) -> None:
def test_read_config_missing_file(tmp_path: Path) -> None:
config_path = tmp_path / "missing.toml"
with pytest.raises(ConfigError, match="Missing config file"):
read_raw_toml(config_path)
read_config(config_path)
def test_read_raw_toml_invalid_toml(tmp_path: Path) -> None:
def test_read_config_invalid_toml(tmp_path: Path) -> None:
config_path = tmp_path / "takopi.toml"
config_path.write_text("nope = [", encoding="utf-8")
with pytest.raises(ConfigError, match="Malformed TOML"):
read_raw_toml(config_path)
read_config(config_path)
def test_read_raw_toml_non_file(tmp_path: Path) -> None:
def test_read_config_non_file(tmp_path: Path) -> None:
config_path = tmp_path / "config_dir"
config_path.mkdir()
with pytest.raises(ConfigError, match="exists but is not a file"):
read_raw_toml(config_path)
read_config(config_path)
+18 -9
View File
@@ -4,8 +4,8 @@ import anyio
import pytest
import takopi.config_watch as config_watch
from takopi.config_watch import ConfigReload, _config_status, watch_config
from takopi.config import empty_projects_config
from takopi.config_watch import ConfigReload, config_status, watch_config
from takopi.config import ProjectsConfig
from takopi.router import AutoRouter, RunnerEntry
from takopi.runtime_loader import RuntimeSpec
from takopi.runners.mock import Return, ScriptRunner
@@ -15,19 +15,23 @@ from takopi.transport_runtime import TransportRuntime
def test_config_status_variants(tmp_path: Path) -> None:
missing = tmp_path / "missing.toml"
status, signature = _config_status(missing)
status, signature = config_status(missing)
assert status == "missing"
assert signature is None
directory = tmp_path / "config.d"
directory.mkdir()
status, signature = _config_status(directory)
status, signature = config_status(directory)
assert status == "invalid"
assert signature is None
config_file = tmp_path / "takopi.toml"
config_file.write_text('transport = "telegram"\n', encoding="utf-8")
status, signature = _config_status(config_file)
config_file.write_text(
'transport = "telegram"\n\n[transports.telegram]\n'
'bot_token = "token"\nchat_id = 123\n',
encoding="utf-8",
)
status, signature = config_status(config_file)
assert status == "ok"
assert signature is not None
@@ -47,7 +51,7 @@ async def test_watch_config_applies_runtime(
)
runtime = TransportRuntime(
router=router,
projects=empty_projects_config(),
projects=ProjectsConfig(projects={}, default_project=None),
config_path=resolved_path,
)
@@ -58,12 +62,17 @@ async def test_watch_config_applies_runtime(
)
new_spec = RuntimeSpec(
router=new_router,
projects=empty_projects_config(),
projects=ProjectsConfig(projects={}, default_project=None),
allowlist=None,
plugin_configs=None,
)
reload = ConfigReload(
settings=TakopiSettings.model_validate({"transport": "telegram"}),
settings=TakopiSettings.model_validate(
{
"transport": "telegram",
"transports": {"telegram": {"bot_token": "token", "chat_id": 123}},
}
),
runtime_spec=new_spec,
config_path=resolved_path,
)
+1 -1
View File
@@ -87,7 +87,7 @@ def test_require_telegram_rejects_empty_token(tmp_path) -> None:
encoding="utf-8",
)
with pytest.raises(ConfigError, match="bot token"):
with pytest.raises(ConfigError, match="bot_token"):
settings, _ = load_settings(config_path)
require_telegram(settings, config_path)
+2 -2
View File
@@ -12,7 +12,7 @@ from takopi.model import (
StartedEvent,
TakopiEvent,
)
from takopi.runners.codex import CodexRunner, _find_exec_only_flag
from takopi.runners.codex import CodexRunner, find_exec_only_flag
CODEX_ENGINE = EngineId("codex")
@@ -159,7 +159,7 @@ def test_codex_exec_flags_after_exec() -> None:
],
)
def test_find_exec_only_flag(extra_args: list[str], expected: str | None) -> None:
assert _find_exec_only_flag(extra_args) == expected
assert find_exec_only_flag(extra_args) == expected
@pytest.mark.anyio
+7 -2
View File
@@ -49,9 +49,13 @@ def test_check_setup_marks_missing_config(monkeypatch, tmp_path: Path) -> None:
assert result.config_path == onboarding.HOME_CONFIG_PATH
def test_check_setup_marks_invalid_chat_id(monkeypatch, tmp_path: Path) -> None:
def test_check_setup_marks_invalid_bot_token(monkeypatch, tmp_path: Path) -> None:
backend = engines.get_backend("codex")
monkeypatch.setattr(onboarding.shutil, "which", lambda _name: "/usr/bin/codex")
def _fail_require(*_args, **_kwargs):
raise onboarding.ConfigError("Missing bot token")
monkeypatch.setattr(
onboarding,
"load_settings",
@@ -59,12 +63,13 @@ def test_check_setup_marks_invalid_chat_id(monkeypatch, tmp_path: Path) -> None:
TakopiSettings.model_validate(
{
"transport": "telegram",
"transports": {"telegram": {"bot_token": "token", "chat_id": None}},
"transports": {"telegram": {"bot_token": "token", "chat_id": 123}},
}
),
tmp_path / "takopi.toml",
),
)
monkeypatch.setattr(onboarding, "require_telegram", _fail_require)
result = onboarding.check_setup(backend)
+25 -17
View File
@@ -1,26 +1,34 @@
from __future__ import annotations
from takopi.config import dump_toml
from takopi.telegram import onboarding
from takopi.backends import EngineBackend
def test_mask_token_short() -> None:
assert onboarding._mask_token("short") == "*****"
assert onboarding.mask_token("short") == "*****"
def test_mask_token_long() -> None:
token = "123456789:ABCdefGH"
masked = onboarding._mask_token(token)
masked = onboarding.mask_token(token)
assert masked.startswith("123456789")
assert masked.endswith("defGH")
assert "..." in masked
def test_render_config_escapes() -> None:
config = onboarding._render_config(
'token"with\\quote',
123,
"codex",
config = dump_toml(
{
"default_engine": "codex",
"transport": "telegram",
"transports": {
"telegram": {
"bot_token": 'token"with\\quote',
"chat_id": 123,
}
},
}
)
assert 'default_engine = "codex"' in config
assert 'transport = "telegram"' in config
@@ -82,9 +90,9 @@ def test_interactive_setup_writes_config(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(onboarding.questionary, "select", _queue(["codex"]))
def _fake_run(func, *args, **kwargs):
if func is onboarding._get_bot_info:
if func is onboarding.get_bot_info:
return {"username": "my_bot"}
if func is onboarding._wait_for_chat:
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=123,
username="alice",
@@ -127,9 +135,9 @@ def test_interactive_setup_preserves_projects(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(onboarding.questionary, "select", _queue(["codex"]))
def _fake_run(func, *args, **kwargs):
if func is onboarding._get_bot_info:
if func is onboarding.get_bot_info:
return {"username": "my_bot"}
if func is onboarding._wait_for_chat:
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=123,
username="alice",
@@ -164,9 +172,9 @@ def test_interactive_setup_no_agents_aborts(monkeypatch, tmp_path) -> None:
)
def _fake_run(func, *args, **kwargs):
if func is onboarding._get_bot_info:
if func is onboarding.get_bot_info:
return {"username": "my_bot"}
if func is onboarding._wait_for_chat:
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=123,
username="alice",
@@ -202,9 +210,9 @@ def test_interactive_setup_recovers_from_malformed_toml(monkeypatch, tmp_path) -
monkeypatch.setattr(onboarding.questionary, "select", _queue(["codex"]))
def _fake_run(func, *args, **kwargs):
if func is onboarding._get_bot_info:
if func is onboarding.get_bot_info:
return {"username": "my_bot"}
if func is onboarding._wait_for_chat:
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=123,
username="alice",
@@ -230,9 +238,9 @@ def test_interactive_setup_recovers_from_malformed_toml(monkeypatch, tmp_path) -
def test_capture_chat_id_with_token(monkeypatch) -> None:
def _fake_run(func, *args, **kwargs):
if func is onboarding._get_bot_info:
if func is onboarding.get_bot_info:
return {"username": "my_bot"}
if func is onboarding._wait_for_chat:
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=456,
username=None,
@@ -257,7 +265,7 @@ def test_capture_chat_id_prompts_for_token(monkeypatch) -> None:
)
def _fake_run(func, *args, **kwargs):
if func is onboarding._wait_for_chat:
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=789,
username="alice",
+20 -7
View File
@@ -4,13 +4,16 @@ import pytest
from typer.testing import CliRunner
from takopi import cli
from takopi.config import ConfigError
from takopi.config_store import read_raw_toml
from takopi.config import ConfigError, read_config
from takopi.settings import TakopiSettings
def _base_config() -> dict:
return {"transports": {"telegram": {"bot_token": "token", "chat_id": 123}}}
def test_parse_projects_rejects_engine_alias() -> None:
config = {"projects": {"codex": {"path": "/tmp/repo"}}}
config = {**_base_config(), "projects": {"codex": {"path": "/tmp/repo"}}}
with pytest.raises(ConfigError, match="aliases must not match engine ids"):
settings = TakopiSettings.model_validate(config)
settings.to_projects_config(
@@ -21,7 +24,7 @@ def test_parse_projects_rejects_engine_alias() -> None:
def test_parse_projects_default_project_must_exist() -> None:
config = {"default_project": "z80", "projects": {}}
config = {**_base_config(), "default_project": "z80", "projects": {}}
with pytest.raises(ConfigError, match="default_project"):
settings = TakopiSettings.model_validate(config)
settings.to_projects_config(
@@ -33,6 +36,11 @@ def test_parse_projects_default_project_must_exist() -> None:
def test_init_writes_project(monkeypatch, tmp_path) -> None:
config_path = tmp_path / "takopi.toml"
config_path.write_text(
'transport = "telegram"\n\n[transports.telegram]\n'
'bot_token = "token"\nchat_id = 123\n',
encoding="utf-8",
)
monkeypatch.setattr("takopi.config.HOME_CONFIG_PATH", config_path)
monkeypatch.setattr(cli, "resolve_default_base", lambda _: "main")
monkeypatch.setattr(cli, "_load_settings_optional", lambda: (None, None))
@@ -67,7 +75,7 @@ def test_init_migrates_legacy_config(monkeypatch, tmp_path) -> None:
result = runner.invoke(cli.create_app(), ["init", "z80"])
assert result.exit_code == 0
raw = read_raw_toml(config_path)
raw = read_config(config_path)
assert "bot_token" not in raw
assert "chat_id" not in raw
assert raw["transport"] == "telegram"
@@ -77,7 +85,10 @@ def test_init_migrates_legacy_config(monkeypatch, tmp_path) -> None:
def test_projects_default_engine_unknown() -> None:
config = {"projects": {"z80": {"path": "/tmp/repo", "default_engine": "nope"}}}
config = {
**_base_config(),
"projects": {"z80": {"path": "/tmp/repo", "default_engine": "nope"}},
}
settings = TakopiSettings.model_validate(config)
with pytest.raises(ConfigError, match="projects.z80.default_engine"):
settings.to_projects_config(
@@ -120,7 +131,9 @@ def test_projects_chat_id_must_be_unique() -> None:
def test_projects_relative_path_resolves(tmp_path: Path) -> None:
config_path = tmp_path / "takopi.toml"
settings = TakopiSettings.model_validate({"projects": {"z80": {"path": "repo"}}})
settings = TakopiSettings.model_validate(
{**_base_config(), "projects": {"z80": {"path": "repo"}}}
)
projects = settings.to_projects_config(
config_path=config_path,
engine_ids=["codex"],
+19 -3
View File
@@ -11,9 +11,19 @@ def test_build_runtime_spec_minimal(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(runtime_loader.shutil, "which", lambda _cmd: "/bin/echo")
settings = TakopiSettings.model_validate({"transport": "telegram"})
settings = TakopiSettings.model_validate(
{
"transport": "telegram",
"watch_config": True,
"transports": {"telegram": {"bot_token": "token", "chat_id": 123}},
}
)
config_path = tmp_path / "takopi.toml"
config_path.write_text('transport = "telegram"\n', encoding="utf-8")
config_path.write_text(
'transport = "telegram"\n\n[transports.telegram]\n'
'bot_token = "token"\nchat_id = 123\n',
encoding="utf-8",
)
spec = runtime_loader.build_runtime_spec(
settings=settings,
@@ -23,10 +33,16 @@ def test_build_runtime_spec_minimal(
assert spec.router.default_engine == settings.default_engine
runtime = spec.to_runtime(config_path=config_path)
assert runtime.default_engine == settings.default_engine
assert runtime.watch_config is True
def test_resolve_default_engine_unknown(tmp_path: Path) -> None:
settings = TakopiSettings.model_validate({"transport": "telegram"})
settings = TakopiSettings.model_validate(
{
"transport": "telegram",
"transports": {"telegram": {"bot_token": "token", "chat_id": 123}},
}
)
with pytest.raises(ConfigError, match="Unknown default engine"):
runtime_loader.resolve_default_engine(
override="unknown",
+19 -15
View File
@@ -4,8 +4,7 @@ from pathlib import Path
import pytest
from takopi.config import ConfigError
from takopi.config_store import read_raw_toml
from takopi.config import ConfigError, read_config
from takopi.settings import (
TakopiSettings,
load_settings,
@@ -38,8 +37,7 @@ def test_load_settings_from_toml(tmp_path: Path) -> None:
assert token == "token"
assert chat_id == 123
dumped = settings.model_dump()
assert dumped["transports"]["telegram"]["bot_token"] == "token"
assert settings.transports.telegram.bot_token == "token"
def test_env_overrides_toml(tmp_path: Path, monkeypatch) -> None:
@@ -67,7 +65,7 @@ def test_legacy_keys_migrated(tmp_path: Path) -> None:
assert loaded_path == config_path
assert settings.transports.telegram.chat_id == 123
raw = read_raw_toml(config_path)
raw = read_config(config_path)
assert "bot_token" not in raw
assert "chat_id" not in raw
assert raw["transports"]["telegram"]["bot_token"] == "token"
@@ -100,7 +98,10 @@ def test_validate_settings_data_rejects_empty_default_engine(tmp_path: Path) ->
def test_validate_settings_data_rejects_empty_default_project(tmp_path: Path) -> None:
config_path = tmp_path / "takopi.toml"
data = {"default_project": " "}
data = {
"default_project": " ",
"transports": {"telegram": {"bot_token": "token", "chat_id": 123}},
}
with pytest.raises(ConfigError, match="default_project"):
validate_settings_data(data, config_path=config_path)
@@ -108,7 +109,10 @@ def test_validate_settings_data_rejects_empty_default_project(tmp_path: Path) ->
def test_validate_settings_data_rejects_empty_project_path(tmp_path: Path) -> None:
config_path = tmp_path / "takopi.toml"
data = {"projects": {"z80": {"path": " "}}}
data = {
"projects": {"z80": {"path": " "}},
"transports": {"telegram": {"bot_token": "token", "chat_id": 123}},
}
with pytest.raises(ConfigError, match="path"):
validate_settings_data(data, config_path=config_path)
@@ -172,14 +176,14 @@ def test_transport_config_telegram_and_extra(tmp_path: Path) -> None:
settings.transport_config("discord", config_path=config_path)
def test_bot_token_none_allowed() -> None:
settings = TakopiSettings.model_validate(
{
"transport": "telegram",
"transports": {"telegram": {"bot_token": None, "chat_id": 123}},
}
)
assert settings.transports.telegram.bot_token is None
def test_bot_token_none_rejected(tmp_path: Path) -> None:
config_path = tmp_path / "takopi.toml"
data = {
"transport": "telegram",
"transports": {"telegram": {"bot_token": None, "chat_id": 123}},
}
with pytest.raises(ConfigError, match="bot_token"):
validate_settings_data(data, config_path=config_path)
def test_require_telegram_rejects_non_telegram_transport(tmp_path: Path) -> None:
+30
View File
@@ -0,0 +1,30 @@
from pathlib import Path
import pytest
from takopi.config import ConfigError
from takopi.settings import TakopiSettings, validate_settings_data
def test_settings_strips_and_expands_transport_config(tmp_path: Path) -> None:
settings = TakopiSettings.model_validate(
{
"transport": " telegram ",
"plugins": {"enabled": [" foo "]},
"transports": {"telegram": {"bot_token": " token ", "chat_id": 123}},
}
)
assert settings.transport == "telegram"
assert settings.plugins.enabled == ["foo"]
assert settings.transports.telegram.bot_token == "token"
def test_settings_rejects_bool_chat_id(tmp_path: Path) -> None:
data = {
"transport": "telegram",
"transports": {"telegram": {"bot_token": "token", "chat_id": True}},
}
with pytest.raises(ConfigError, match="chat_id"):
validate_settings_data(data, config_path=tmp_path / "takopi.toml")
+18 -13
View File
@@ -5,7 +5,7 @@ from typing import Any
import pytest
from takopi.config import ConfigError, empty_projects_config
from takopi.config import ProjectsConfig
from takopi.model import EngineId
from takopi.router import AutoRouter, RunnerEntry
from takopi.runners.mock import Return, ScriptRunner
@@ -25,7 +25,11 @@ def test_build_startup_message_includes_missing_engines(tmp_path: Path) -> None:
],
default_engine=codex,
)
runtime = TransportRuntime(router=router, projects=empty_projects_config())
runtime = TransportRuntime(
router=router,
projects=ProjectsConfig(projects={}, default_project=None),
watch_config=True,
)
message = telegram_backend._build_startup_message(
runtime, startup_pwd=str(tmp_path)
@@ -54,7 +58,11 @@ def test_telegram_backend_build_and_run_wires_config(
entries=[RunnerEntry(engine=codex, runner=runner, available=True)],
default_engine=codex,
)
runtime = TransportRuntime(router=router, projects=empty_projects_config())
runtime = TransportRuntime(
router=router,
projects=ProjectsConfig(projects={}, default_project=None),
watch_config=True,
)
captured: dict[str, Any] = {}
@@ -91,8 +99,7 @@ def test_telegram_backend_build_and_run_wires_config(
cfg = captured["cfg"]
kwargs = captured["kwargs"]
assert cfg.chat_id == 321
assert cfg.voice_transcription is not None
assert cfg.voice_transcription.enabled is True
assert cfg.voice_transcription is True
assert cfg.files.enabled is True
assert cfg.files.allowed_user_ids == frozenset({1, 2})
assert cfg.topics.enabled is True
@@ -101,12 +108,10 @@ def test_telegram_backend_build_and_run_wires_config(
assert kwargs["transport_id"] == "telegram"
def test_build_files_config_rejects_non_dict(tmp_path: Path) -> None:
config_path = tmp_path / "takopi.toml"
transport_config: dict[str, object] = {"files": ["nope"]}
def test_build_files_config_defaults() -> None:
cfg = telegram_backend._build_files_config({})
with pytest.raises(ConfigError, match="transports.telegram.files"):
telegram_backend._build_files_config(
transport_config,
config_path=config_path,
)
assert cfg.enabled is False
assert cfg.auto_put is True
assert cfg.uploads_dir == "incoming"
assert cfg.allowed_user_ids == frozenset()
+52 -47
View File
@@ -7,23 +7,26 @@ import pytest
from takopi import commands, plugins
import takopi.telegram.bridge as bridge
import takopi.telegram.loop as telegram_loop
import takopi.telegram.commands as telegram_commands
import takopi.telegram.topics as telegram_topics
from takopi.directives import parse_directives
from takopi.telegram.bridge import (
TelegramBridgeConfig,
TelegramFilesConfig,
TelegramPresenter,
TelegramTransport,
_build_bot_commands,
_handle_callback_cancel,
_handle_cancel,
_is_cancel_command,
_send_with_resume,
build_bot_commands,
handle_callback_cancel,
handle_cancel,
is_cancel_command,
send_with_resume,
run_main_loop,
)
from takopi.telegram.client import BotClient
from takopi.telegram.topic_state import TopicStateStore, resolve_state_path
from takopi.context import RunContext
from takopi.config import ProjectConfig, ProjectsConfig, empty_projects_config
from takopi.config import ProjectConfig, ProjectsConfig
from takopi.runner_bridge import ExecBridgeConfig, RunningTask
from takopi.markdown import MarkdownPresenter
from takopi.model import EngineId, ResumeToken
@@ -42,6 +45,10 @@ from tests.plugin_fixtures import FakeEntryPoint, install_entrypoints
CODEX_ENGINE = EngineId("codex")
def _empty_projects() -> ProjectsConfig:
return ProjectsConfig(projects={}, default_project=None)
def _make_router(runner) -> AutoRouter:
return AutoRouter(
entries=[RunnerEntry(engine=runner.engine, runner=runner)],
@@ -288,7 +295,7 @@ def _make_cfg(
)
runtime = TransportRuntime(
router=_make_router(runner),
projects=empty_projects_config(),
projects=_empty_projects(),
)
return TelegramBridgeConfig(
bot=_FakeBot(),
@@ -303,7 +310,7 @@ def test_parse_directives_inline_engine() -> None:
directives = parse_directives(
"/claude do it",
engine_ids=("codex", "claude"),
projects=empty_projects_config(),
projects=_empty_projects(),
)
assert directives.engine == "claude"
assert directives.prompt == "do it"
@@ -313,7 +320,7 @@ def test_parse_directives_newline() -> None:
directives = parse_directives(
"/codex\nhello",
engine_ids=("codex", "claude"),
projects=empty_projects_config(),
projects=_empty_projects(),
)
assert directives.engine == "codex"
assert directives.prompt == "hello"
@@ -323,7 +330,7 @@ def test_parse_directives_ignores_unknown() -> None:
directives = parse_directives(
"/unknown hi",
engine_ids=("codex", "claude"),
projects=empty_projects_config(),
projects=_empty_projects(),
)
assert directives.engine is None
assert directives.prompt == "/unknown hi"
@@ -333,7 +340,7 @@ def test_parse_directives_bot_suffix() -> None:
directives = parse_directives(
"/claude@bunny_agent_bot hi",
engine_ids=("claude",),
projects=empty_projects_config(),
projects=_empty_projects(),
)
assert directives.engine == "claude"
assert directives.prompt == "hi"
@@ -343,7 +350,7 @@ def test_parse_directives_only_first_non_empty_line() -> None:
directives = parse_directives(
"hello\n/claude hi",
engine_ids=("codex", "claude"),
projects=empty_projects_config(),
projects=_empty_projects(),
)
assert directives.engine is None
assert directives.prompt == "hello\n/claude hi"
@@ -355,9 +362,9 @@ def test_build_bot_commands_includes_cancel_and_engine() -> None:
)
runtime = TransportRuntime(
router=_make_router(runner),
projects=empty_projects_config(),
projects=_empty_projects(),
)
commands = _build_bot_commands(runtime)
commands = build_bot_commands(runtime)
assert {"command": "cancel", "description": "cancel run"} in commands
assert {"command": "file", "description": "upload or fetch files"} in commands
@@ -386,7 +393,7 @@ def test_build_bot_commands_includes_projects() -> None:
)
runtime = TransportRuntime(router=router, projects=projects)
commands = _build_bot_commands(runtime)
commands = build_bot_commands(runtime)
assert any(cmd["command"] == "good" for cmd in commands)
assert not any(cmd["command"] == "bad-name" for cmd in commands)
@@ -413,10 +420,10 @@ def test_build_bot_commands_includes_command_plugins(monkeypatch) -> None:
runner = ScriptRunner([Return(answer="ok")], engine=CODEX_ENGINE)
runtime = TransportRuntime(
router=_make_router(runner),
projects=empty_projects_config(),
projects=_empty_projects(),
)
commands_list = _build_bot_commands(runtime)
commands_list = build_bot_commands(runtime)
assert {"command": "pingcmd", "description": "ping command"} in commands_list
@@ -439,7 +446,7 @@ def test_build_bot_commands_caps_total() -> None:
)
runtime = TransportRuntime(router=router, projects=projects)
commands = _build_bot_commands(runtime)
commands = build_bot_commands(runtime)
assert len(commands) == 100
assert any(cmd["command"] == "codex" for cmd in commands)
@@ -667,7 +674,7 @@ async def test_handle_cancel_without_reply_prompts_user() -> None:
)
running_tasks: dict = {}
await _handle_cancel(cfg, msg, running_tasks)
await handle_cancel(cfg, msg, running_tasks)
assert len(transport.send_calls) == 1
assert "reply to the progress message" in transport.send_calls[0]["message"].text
@@ -688,7 +695,7 @@ async def test_handle_cancel_with_no_progress_message_says_nothing_running() ->
)
running_tasks: dict = {}
await _handle_cancel(cfg, msg, running_tasks)
await handle_cancel(cfg, msg, running_tasks)
assert len(transport.send_calls) == 1
assert "nothing is currently running" in transport.send_calls[0]["message"].text
@@ -710,7 +717,7 @@ async def test_handle_cancel_with_finished_task_says_nothing_running() -> None:
)
running_tasks: dict = {}
await _handle_cancel(cfg, msg, running_tasks)
await handle_cancel(cfg, msg, running_tasks)
assert len(transport.send_calls) == 1
assert "nothing is currently running" in transport.send_calls[0]["message"].text
@@ -733,7 +740,7 @@ async def test_handle_cancel_cancels_running_task() -> None:
running_task = RunningTask()
running_tasks = {MessageRef(channel_id=123, message_id=progress_id): running_task}
await _handle_cancel(cfg, msg, running_tasks)
await handle_cancel(cfg, msg, running_tasks)
assert running_task.cancel_requested.is_set() is True
assert len(transport.send_calls) == 0 # No error message sent
@@ -759,7 +766,7 @@ async def test_handle_cancel_only_cancels_matching_progress_message() -> None:
MessageRef(channel_id=123, message_id=2): task_second,
}
await _handle_cancel(cfg, msg, running_tasks)
await handle_cancel(cfg, msg, running_tasks)
assert task_first.cancel_requested.is_set() is True
assert task_second.cancel_requested.is_set() is False
@@ -824,7 +831,9 @@ async def test_handle_file_put_writes_file(tmp_path: Path) -> None:
),
)
await bridge._handle_file_put(cfg, msg, "/proj uploads/hello.txt", None, None)
await telegram_commands._handle_file_put(
cfg, msg, "/proj uploads/hello.txt", None, None
)
target = tmp_path / "uploads" / "hello.txt"
assert target.read_bytes() == payload
@@ -883,7 +892,7 @@ async def test_handle_file_get_sends_document_for_allowed_user(
chat_type="supergroup",
)
await bridge._handle_file_get(cfg, msg, "/proj hello.txt", None, None)
await telegram_commands._handle_file_get(cfg, msg, "/proj hello.txt", None, None)
assert bot.document_calls
assert bot.document_calls[0]["filename"] == "hello.txt"
@@ -906,7 +915,7 @@ async def test_handle_callback_cancel_cancels_running_task() -> None:
sender_id=123,
)
await _handle_callback_cancel(cfg, query, running_tasks)
await handle_callback_cancel(cfg, query, running_tasks)
assert running_task.cancel_requested.is_set() is True
assert len(transport.send_calls) == 0
@@ -928,7 +937,7 @@ async def test_handle_callback_cancel_without_task_acknowledges() -> None:
sender_id=123,
)
await _handle_callback_cancel(cfg, query, {})
await handle_callback_cancel(cfg, query, {})
assert len(transport.send_calls) == 0
bot = cast(_FakeBot, cfg.bot)
@@ -937,9 +946,9 @@ async def test_handle_callback_cancel_without_task_acknowledges() -> None:
def test_cancel_command_accepts_extra_text() -> None:
assert _is_cancel_command("/cancel now") is True
assert _is_cancel_command("/cancel@takopi please") is True
assert _is_cancel_command("/cancelled") is False
assert is_cancel_command("/cancel now") is True
assert is_cancel_command("/cancel@takopi please") is True
assert is_cancel_command("/cancelled") is False
def test_resolve_message_accepts_backticked_ctx_line() -> None:
@@ -971,24 +980,21 @@ def test_topic_title_matches_command_syntax() -> None:
transport = _FakeTransport()
cfg = _make_cfg(transport)
title = bridge._topic_title(
cfg=cfg,
title = telegram_topics._topic_title(
runtime=cfg.runtime,
context=RunContext(project="takopi", branch="master"),
)
assert title == "takopi @master"
title = bridge._topic_title(
cfg=cfg,
title = telegram_topics._topic_title(
runtime=cfg.runtime,
context=RunContext(project="takopi", branch=None),
)
assert title == "takopi"
title = bridge._topic_title(
cfg=cfg,
title = telegram_topics._topic_title(
runtime=cfg.runtime,
context=RunContext(project=None, branch="main"),
)
@@ -1006,8 +1012,7 @@ def test_topic_title_projects_scope_includes_project() -> None:
),
)
title = bridge._topic_title(
cfg=cfg,
title = telegram_topics._topic_title(
runtime=cfg.runtime,
context=RunContext(project="takopi", branch="master"),
)
@@ -1028,7 +1033,7 @@ async def test_maybe_rename_topic_updates_title(tmp_path: Path) -> None:
topic_title="takopi @old",
)
await bridge._maybe_rename_topic(
await telegram_topics._maybe_rename_topic(
cfg,
store,
chat_id=123,
@@ -1058,7 +1063,7 @@ async def test_maybe_rename_topic_skips_when_title_matches(tmp_path: Path) -> No
)
snapshot = await store.get_thread(123, 77)
await bridge._maybe_rename_topic(
await telegram_topics._maybe_rename_topic(
cfg,
store,
chat_id=123,
@@ -1096,7 +1101,7 @@ async def test_send_with_resume_waits_for_token() -> None:
async with anyio.create_task_group() as tg:
tg.start_soon(trigger_resume)
await _send_with_resume(
await send_with_resume(
cfg,
enqueue,
running_task,
@@ -1138,7 +1143,7 @@ async def test_send_with_resume_reports_when_missing() -> None:
running_task = RunningTask()
running_task.done.set()
await _send_with_resume(
await send_with_resume(
cfg,
enqueue,
running_task,
@@ -1175,7 +1180,7 @@ async def test_run_main_loop_routes_reply_to_running_resume() -> None:
)
runtime = TransportRuntime(
router=_make_router(runner),
projects=empty_projects_config(),
projects=_empty_projects(),
)
cfg = TelegramBridgeConfig(
bot=bot,
@@ -1311,7 +1316,7 @@ async def test_run_main_loop_replies_in_same_thread() -> None:
)
runtime = TransportRuntime(
router=_make_router(runner),
projects=empty_projects_config(),
projects=_empty_projects(),
)
cfg = TelegramBridgeConfig(
bot=bot,
@@ -1489,7 +1494,7 @@ async def test_run_main_loop_handles_command_plugins(monkeypatch) -> None:
)
runtime = TransportRuntime(
router=_make_router(runner),
projects=empty_projects_config(),
projects=_empty_projects(),
)
cfg = TelegramBridgeConfig(
bot=bot,
@@ -1714,7 +1719,7 @@ async def test_run_main_loop_refreshes_command_ids(monkeypatch) -> None:
return []
return ["late_cmd"]
monkeypatch.setattr(bridge, "list_command_ids", _list_command_ids)
monkeypatch.setattr(telegram_loop, "list_command_ids", _list_command_ids)
transport = _FakeTransport()
bot = _FakeBot()
@@ -1726,7 +1731,7 @@ async def test_run_main_loop_refreshes_command_ids(monkeypatch) -> None:
)
runtime = TransportRuntime(
router=_make_router(runner),
projects=empty_projects_config(),
projects=_empty_projects(),
)
cfg = TelegramBridgeConfig(
bot=bot,