feat: onboarding overhaul, persona-based setup (#132)

This commit is contained in:
banteg
2026-01-15 03:28:37 +04:00
committed by GitHub
parent a1a2714c01
commit ffae80dce7
32 changed files with 1870 additions and 445 deletions
+146 -128
View File
@@ -1,5 +1,8 @@
from __future__ import annotations
import anyio
from functools import partial
from takopi.backends import EngineBackend
from takopi.config import dump_toml
from takopi.telegram import onboarding
@@ -39,32 +42,56 @@ def test_render_config_escapes() -> None:
assert config.endswith("\n")
class _FakeQuestion:
class FakeQuestion:
def __init__(self, value):
self._value = value
def ask(self):
return self._value
async def ask_async(self):
return self._value
def _queue(values):
def queue_answers(values):
it = iter(values)
def _make(*_args, **_kwargs):
return _FakeQuestion(next(it))
return FakeQuestion(next(it))
return _make
def _queue_values(values):
def queue_values(values):
it = iter(values)
def _next(*_args, **_kwargs):
async def _next(*_args, **_kwargs):
return next(it)
return _next
def patch_live_services(
monkeypatch,
*,
bot: User,
chat: onboarding.ChatInfo,
topics_issue=None,
) -> None:
async def _get_bot_info(self, _token: str):
return bot
async def _wait_for_chat(self, _token: str):
return chat
async def _validate_topics(self, _token: str, _chat_id: int, _scope):
return topics_issue
monkeypatch.setattr(onboarding.LiveServices, "get_bot_info", _get_bot_info)
monkeypatch.setattr(onboarding.LiveServices, "wait_for_chat", _wait_for_chat)
monkeypatch.setattr(onboarding.LiveServices, "validate_topics", _validate_topics)
def test_interactive_setup_skips_when_config_exists(monkeypatch, tmp_path) -> None:
config_path = tmp_path / "takopi.toml"
config_path.write_text(
@@ -73,7 +100,7 @@ def test_interactive_setup_skips_when_config_exists(monkeypatch, tmp_path) -> No
encoding="utf-8",
)
monkeypatch.setattr(onboarding, "HOME_CONFIG_PATH", config_path)
assert onboarding.interactive_setup(force=False) is True
assert anyio.run(partial(onboarding.interactive_setup, force=False)) is True
def test_interactive_setup_writes_config(monkeypatch, tmp_path) -> None:
@@ -84,36 +111,38 @@ def test_interactive_setup_writes_config(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(onboarding, "list_backends", lambda: [backend])
monkeypatch.setattr(onboarding.shutil, "which", lambda _cmd: "/usr/bin/codex")
monkeypatch.setattr(onboarding, "_confirm", _queue_values([True, True]))
monkeypatch.setattr(onboarding, "confirm_prompt", queue_values([True, True]))
monkeypatch.setattr(
onboarding.questionary, "password", _queue(["123456789:ABCdef"])
onboarding.questionary, "password", queue_answers(["123456789:ABCdef"])
)
monkeypatch.setattr(
onboarding.questionary,
"select",
queue_answers(["assistant", "codex"]),
)
patch_live_services(
monkeypatch,
bot=User(id=1, username="my_bot"),
chat=onboarding.ChatInfo(
chat_id=123,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
),
)
monkeypatch.setattr(onboarding.questionary, "select", _queue(["codex"]))
def _fake_run(func, *args, **kwargs):
if func is onboarding.get_bot_info:
return User(id=1, username="my_bot")
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=123,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
)
if func is onboarding._send_confirmation:
return True
raise AssertionError(f"unexpected anyio.run target: {func}")
monkeypatch.setattr(onboarding.anyio, "run", _fake_run)
assert onboarding.interactive_setup(force=False) is True
assert anyio.run(partial(onboarding.interactive_setup, force=False)) is True
saved = config_path.read_text(encoding="utf-8")
assert 'transport = "telegram"' in saved
assert "[transports.telegram]" in saved
assert 'bot_token = "123456789:ABCdef"' in saved
assert "chat_id = 123" in saved
assert 'session_mode = "chat"' in saved
assert "show_resume_line = false" in saved
assert "[transports.telegram.topics]" in saved
assert "enabled = false" in saved
assert 'default_engine = "codex"' in saved
@@ -129,31 +158,29 @@ def test_interactive_setup_preserves_projects(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(onboarding, "list_backends", lambda: [backend])
monkeypatch.setattr(onboarding.shutil, "which", lambda _cmd: "/usr/bin/codex")
monkeypatch.setattr(onboarding, "_confirm", _queue_values([True, True, True]))
monkeypatch.setattr(onboarding, "confirm_prompt", queue_values([True, True, True]))
monkeypatch.setattr(
onboarding.questionary, "password", _queue(["123456789:ABCdef"])
onboarding.questionary, "password", queue_answers(["123456789:ABCdef"])
)
monkeypatch.setattr(
onboarding.questionary,
"select",
queue_answers(["assistant", "codex"]),
)
patch_live_services(
monkeypatch,
bot=User(id=1, username="my_bot"),
chat=onboarding.ChatInfo(
chat_id=123,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
),
)
monkeypatch.setattr(onboarding.questionary, "select", _queue(["codex"]))
def _fake_run(func, *args, **kwargs):
if func is onboarding.get_bot_info:
return User(id=1, username="my_bot")
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=123,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
)
if func is onboarding._send_confirmation:
return True
raise AssertionError(f"unexpected anyio.run target: {func}")
monkeypatch.setattr(onboarding.anyio, "run", _fake_run)
assert onboarding.interactive_setup(force=True) is True
assert anyio.run(partial(onboarding.interactive_setup, force=True)) is True
saved = config_path.read_text(encoding="utf-8")
assert "[projects.z80]" in saved
assert 'path = "/tmp/repo"' in saved
@@ -167,30 +194,29 @@ def test_interactive_setup_no_agents_aborts(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(onboarding, "list_backends", lambda: [backend])
monkeypatch.setattr(onboarding.shutil, "which", lambda _cmd: None)
monkeypatch.setattr(onboarding, "_confirm", _queue_values([True, False]))
monkeypatch.setattr(onboarding, "confirm_prompt", queue_values([True, False]))
monkeypatch.setattr(
onboarding.questionary, "password", _queue(["123456789:ABCdef"])
onboarding.questionary, "password", queue_answers(["123456789:ABCdef"])
)
monkeypatch.setattr(
onboarding.questionary,
"select",
queue_answers(["assistant"]),
)
patch_live_services(
monkeypatch,
bot=User(id=1, username="my_bot"),
chat=onboarding.ChatInfo(
chat_id=123,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
),
)
def _fake_run(func, *args, **kwargs):
if func is onboarding.get_bot_info:
return User(id=1, username="my_bot")
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=123,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
)
if func is onboarding._send_confirmation:
return True
raise AssertionError(f"unexpected anyio.run target: {func}")
monkeypatch.setattr(onboarding.anyio, "run", _fake_run)
assert onboarding.interactive_setup(force=False) is False
assert anyio.run(partial(onboarding.interactive_setup, force=False)) is False
assert not config_path.exists()
@@ -204,31 +230,29 @@ def test_interactive_setup_recovers_from_malformed_toml(monkeypatch, tmp_path) -
monkeypatch.setattr(onboarding, "list_backends", lambda: [backend])
monkeypatch.setattr(onboarding.shutil, "which", lambda _cmd: "/usr/bin/codex")
monkeypatch.setattr(onboarding, "_confirm", _queue_values([True, True, True]))
monkeypatch.setattr(onboarding, "confirm_prompt", queue_values([True, True, True]))
monkeypatch.setattr(
onboarding.questionary, "password", _queue(["123456789:ABCdef"])
onboarding.questionary, "password", queue_answers(["123456789:ABCdef"])
)
monkeypatch.setattr(
onboarding.questionary,
"select",
queue_answers(["assistant", "codex"]),
)
patch_live_services(
monkeypatch,
bot=User(id=1, username="my_bot"),
chat=onboarding.ChatInfo(
chat_id=123,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
),
)
monkeypatch.setattr(onboarding.questionary, "select", _queue(["codex"]))
def _fake_run(func, *args, **kwargs):
if func is onboarding.get_bot_info:
return User(id=1, username="my_bot")
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=123,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
)
if func is onboarding._send_confirmation:
return True
raise AssertionError(f"unexpected anyio.run target: {func}")
monkeypatch.setattr(onboarding.anyio, "run", _fake_run)
assert onboarding.interactive_setup(force=True) is True
assert anyio.run(partial(onboarding.interactive_setup, force=True)) is True
backup = config_path.with_suffix(".toml.bak")
assert backup.exists()
assert backup.read_text(encoding="utf-8") == bad_toml
@@ -238,50 +262,44 @@ 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:
return User(id=1, username="my_bot")
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=456,
username=None,
title="takopi",
first_name=None,
last_name=None,
chat_type="supergroup",
)
raise AssertionError(f"unexpected anyio.run target: {func}")
patch_live_services(
monkeypatch,
bot=User(id=1, username="my_bot"),
chat=onboarding.ChatInfo(
chat_id=456,
username=None,
title="takopi",
first_name=None,
last_name=None,
chat_type="supergroup",
),
)
monkeypatch.setattr(onboarding.anyio, "run", _fake_run)
chat = onboarding.capture_chat_id(token="123456789:ABCdef")
chat = anyio.run(partial(onboarding.capture_chat_id, token="123456789:ABCdef"))
assert chat is not None
assert chat.chat_id == 456
def test_capture_chat_id_prompts_for_token(monkeypatch) -> None:
monkeypatch.setattr(
onboarding,
"_prompt_token",
lambda _console: ("token", User(id=1, username="bot")),
async def _prompt_token(_ui, _svc):
return ("token", User(id=1, username="bot"))
monkeypatch.setattr(onboarding, "prompt_token", _prompt_token)
patch_live_services(
monkeypatch,
bot=User(id=1, username="bot"),
chat=onboarding.ChatInfo(
chat_id=789,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
),
)
def _fake_run(func, *args, **kwargs):
if func is onboarding.wait_for_chat:
return onboarding.ChatInfo(
chat_id=789,
username="alice",
title=None,
first_name="Alice",
last_name=None,
chat_type="private",
)
raise AssertionError(f"unexpected anyio.run target: {func}")
monkeypatch.setattr(onboarding.anyio, "run", _fake_run)
chat = onboarding.capture_chat_id()
chat = anyio.run(onboarding.capture_chat_id)
assert chat is not None
assert chat.chat_id == 789