fix(config): harden onboarding and command menu (#70)
This commit is contained in:
@@ -9,6 +9,7 @@ from takopi.model import EngineId, ResumeToken, TakopiEvent
|
||||
from takopi.telegram.render import prepare_telegram
|
||||
from takopi.runners.codex import CodexRunner
|
||||
from takopi.runners.mock import Advance, Emit, Raise, Return, ScriptRunner, Wait
|
||||
from takopi.settings import load_settings, require_telegram
|
||||
from takopi.transport import MessageRef, RenderedMessage, SendOptions
|
||||
from tests.factories import action_completed, action_started
|
||||
|
||||
@@ -76,8 +77,8 @@ def _return_runner(
|
||||
)
|
||||
|
||||
|
||||
def test_load_and_validate_config_rejects_empty_token(tmp_path) -> None:
|
||||
from takopi import cli
|
||||
def test_require_telegram_rejects_empty_token(tmp_path) -> None:
|
||||
from takopi.config import ConfigError
|
||||
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
config_path.write_text(
|
||||
@@ -86,12 +87,13 @@ def test_load_and_validate_config_rejects_empty_token(tmp_path) -> None:
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(cli.ConfigError, match="bot token"):
|
||||
cli.load_and_validate_config(config_path)
|
||||
with pytest.raises(ConfigError, match="bot token"):
|
||||
settings, _ = load_settings(config_path)
|
||||
require_telegram(settings, config_path)
|
||||
|
||||
|
||||
def test_load_and_validate_config_rejects_string_chat_id(tmp_path) -> None:
|
||||
from takopi import cli
|
||||
def test_load_settings_rejects_string_chat_id(tmp_path) -> None:
|
||||
from takopi.config import ConfigError
|
||||
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
config_path.write_text(
|
||||
@@ -100,8 +102,8 @@ def test_load_and_validate_config_rejects_string_chat_id(tmp_path) -> None:
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(cli.ConfigError, match="chat_id"):
|
||||
cli.load_and_validate_config(config_path)
|
||||
with pytest.raises(ConfigError, match="chat_id"):
|
||||
load_settings(config_path)
|
||||
|
||||
|
||||
def test_codex_extract_resume_finds_command() -> None:
|
||||
|
||||
@@ -32,9 +32,10 @@ def test_check_setup_marks_missing_codex(monkeypatch, tmp_path: Path) -> None:
|
||||
assert result.ok is False
|
||||
|
||||
|
||||
def test_check_setup_marks_missing_config(monkeypatch) -> None:
|
||||
def test_check_setup_marks_missing_config(monkeypatch, tmp_path: Path) -> None:
|
||||
backend = engines.get_backend("codex")
|
||||
monkeypatch.setattr(onboarding.shutil, "which", lambda _name: "/usr/bin/codex")
|
||||
monkeypatch.setattr(onboarding, "HOME_CONFIG_PATH", tmp_path / "takopi.toml")
|
||||
|
||||
def _raise() -> None:
|
||||
raise onboarding.ConfigError("Missing config file")
|
||||
@@ -68,4 +69,4 @@ def test_check_setup_marks_invalid_chat_id(monkeypatch, tmp_path: Path) -> None:
|
||||
result = onboarding.check_setup(backend)
|
||||
|
||||
titles = {issue.title for issue in result.issues}
|
||||
assert "create a config" in titles
|
||||
assert "configure telegram" in titles
|
||||
|
||||
@@ -148,3 +148,81 @@ def test_interactive_setup_preserves_projects(monkeypatch, tmp_path) -> None:
|
||||
saved = config_path.read_text(encoding="utf-8")
|
||||
assert "[projects.z80]" in saved
|
||||
assert 'path = "/tmp/repo"' in saved
|
||||
|
||||
|
||||
def test_interactive_setup_no_agents_aborts(monkeypatch, tmp_path) -> None:
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
monkeypatch.setattr(onboarding, "HOME_CONFIG_PATH", config_path)
|
||||
|
||||
backend = EngineBackend(id="codex", build_runner=lambda _cfg, _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.questionary, "password", _queue(["123456789:ABCdef"])
|
||||
)
|
||||
|
||||
def _fake_run(func, *args, **kwargs):
|
||||
if func is onboarding._get_bot_info:
|
||||
return {"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 not config_path.exists()
|
||||
|
||||
|
||||
def test_interactive_setup_recovers_from_malformed_toml(monkeypatch, tmp_path) -> None:
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
bad_toml = 'transport = "telegram"\n[transports\n'
|
||||
config_path.write_text(bad_toml, encoding="utf-8")
|
||||
monkeypatch.setattr(onboarding, "HOME_CONFIG_PATH", config_path)
|
||||
|
||||
backend = EngineBackend(id="codex", build_runner=lambda _cfg, _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.questionary, "password", _queue(["123456789:ABCdef"])
|
||||
)
|
||||
monkeypatch.setattr(onboarding.questionary, "select", _queue(["codex"]))
|
||||
|
||||
def _fake_run(func, *args, **kwargs):
|
||||
if func is onboarding._get_bot_info:
|
||||
return {"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
|
||||
backup = config_path.with_suffix(".toml.bak")
|
||||
assert backup.exists()
|
||||
assert backup.read_text(encoding="utf-8") == bad_toml
|
||||
saved = config_path.read_text(encoding="utf-8")
|
||||
assert "[transports.telegram]" in saved
|
||||
assert 'bot_token = "123456789:ABCdef"' in saved
|
||||
|
||||
@@ -5,6 +5,7 @@ from typer.testing import CliRunner
|
||||
|
||||
from takopi import cli
|
||||
from takopi.config import ConfigError
|
||||
from takopi.config_store import read_raw_toml
|
||||
from takopi.settings import TakopiSettings
|
||||
|
||||
|
||||
@@ -50,6 +51,29 @@ def test_init_writes_project(monkeypatch, tmp_path) -> None:
|
||||
assert 'worktree_base = "main"' in saved
|
||||
|
||||
|
||||
def test_init_migrates_legacy_config(monkeypatch, tmp_path) -> None:
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
config_path.write_text('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")
|
||||
|
||||
repo_path = tmp_path / "repo"
|
||||
repo_path.mkdir()
|
||||
monkeypatch.chdir(repo_path)
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli.app, ["init", "z80"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
raw = read_raw_toml(config_path)
|
||||
assert "bot_token" not in raw
|
||||
assert "chat_id" not in raw
|
||||
assert raw["transport"] == "telegram"
|
||||
assert raw["transports"]["telegram"]["bot_token"] == "token"
|
||||
assert raw["transports"]["telegram"]["chat_id"] == 123
|
||||
assert "z80" in raw.get("projects", {})
|
||||
|
||||
|
||||
def test_projects_default_engine_unknown() -> None:
|
||||
config = {"projects": {"z80": {"path": "/tmp/repo", "default_engine": "nope"}}}
|
||||
settings = TakopiSettings.model_validate(config)
|
||||
|
||||
@@ -136,6 +136,42 @@ def test_engine_config_none_and_invalid(tmp_path: Path) -> None:
|
||||
settings.engine_config("codex", config_path=config_path)
|
||||
|
||||
|
||||
def test_transport_config_telegram_and_extra(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "takopi.toml"
|
||||
settings = TakopiSettings.model_validate(
|
||||
{
|
||||
"transport": "telegram",
|
||||
"transports": {"telegram": {"bot_token": "token", "chat_id": 123}},
|
||||
}
|
||||
)
|
||||
telegram = settings.transport_config("telegram", config_path=config_path)
|
||||
assert telegram["bot_token"] == "token"
|
||||
assert telegram["chat_id"] == 123
|
||||
|
||||
settings = TakopiSettings.model_validate(
|
||||
{
|
||||
"transport": "telegram",
|
||||
"transports": {
|
||||
"telegram": {"bot_token": "token", "chat_id": 123},
|
||||
"discord": None,
|
||||
},
|
||||
}
|
||||
)
|
||||
assert settings.transport_config("discord", config_path=config_path) == {}
|
||||
|
||||
settings = TakopiSettings.model_validate(
|
||||
{
|
||||
"transport": "telegram",
|
||||
"transports": {
|
||||
"telegram": {"bot_token": "token", "chat_id": 123},
|
||||
"discord": "nope",
|
||||
},
|
||||
}
|
||||
)
|
||||
with pytest.raises(ConfigError, match="transports.discord"):
|
||||
settings.transport_config("discord", config_path=config_path)
|
||||
|
||||
|
||||
def test_bot_token_none_allowed() -> None:
|
||||
settings = TakopiSettings.model_validate(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user