feat(telegram): close GAP1 — create_bot persists config to @api so minted bots start (DPLAN-0220)
bot_factory.create_bot now calls set_secret('telegram', bot_id, config,
as_json=True) right after building the config (fail-loud on OSError), so a
newly-minted bot's token reaches the @api store that load_bot_config reads.
The disk write is downgraded to a non-fatal shadow; registry now records
bot_token_ref='@api:telegram/{id}' (TG-LIFE-069).
Proven: new TestCreateBotRoundTrip — create_bot -> @api -> load_bot_config
returns the persisted config; + a fail-loud test (set_secret OSError ->
create_bot returns None). Telegram 454/454, skills 252/252.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEQZXCtgnF3NQtcttTErpq
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0096aef1d2
commit
9af4c8ac05
@@ -53,6 +53,12 @@ PyPI version — not the changelog header.
|
||||
leaking into the repo. (4) **prax-monitor** — `log_streamer` tailed a hardcoded
|
||||
`~/system_logs` while prax writes to the repo-root `system_logs`; now resolves the
|
||||
repo root (honoring `AIPASS_TEST_LOG_DIR`) so the log stream actually delivers.
|
||||
(5) **auto-create (GAP1)** — `create_bot` wrote a new bot's config only to a disk
|
||||
shadow file while the runtime loads its token exclusively from the @api store, so
|
||||
a minted bot started then exited with no config; `create_bot` now calls
|
||||
`set_secret('telegram', bot_id, config, as_json=True)` (fail-loud) so the
|
||||
create→@api→load round-trip works and the mother-bot can mint startable bots. New
|
||||
round-trip + fail-loud tests; telegram suite 454/454.
|
||||
(DPLAN-0220)
|
||||
|
||||
- **seedgo CLI help checkers green-lit non-compliant `--help` output** — the
|
||||
|
||||
@@ -47,6 +47,8 @@ from aipass.prax import logger
|
||||
from aipass.skills.apps.handlers.json import json_handler # noqa: F401
|
||||
|
||||
# Internal imports
|
||||
from aipass.api.apps.modules.secrets import set_secret as _api_set_secret
|
||||
|
||||
from .bot_registry import (
|
||||
deregister_bot,
|
||||
ensure_registry,
|
||||
@@ -441,11 +443,7 @@ def create_bot(
|
||||
)
|
||||
return None
|
||||
|
||||
# Step 4: Write per-bot config to local shadow file.
|
||||
# This is intentional: create_bot writes here first, then the config is
|
||||
# imported into @api (drone @api set-secret) as a separate step. At runtime,
|
||||
# load_bot_config reads exclusively from @api — the local file is the
|
||||
# create-then-import staging artifact, not a runtime config source.
|
||||
# Step 4: Build config and persist to @api secrets store + local shadow.
|
||||
ensure_registry()
|
||||
_BOT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -462,15 +460,23 @@ def create_bot(
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
# Step 4a: Write to @api secrets store (load_bot_config reads from here)
|
||||
try:
|
||||
_api_set_secret("telegram", bot_id, config_data, as_json=True)
|
||||
logger.info("Persisted bot config to @api secrets: telegram/%s", bot_id)
|
||||
except OSError as e:
|
||||
logger.error("create_bot failed: could not persist config to @api for '%s': %s", bot_id, e)
|
||||
return None
|
||||
|
||||
# Step 4b: Write local shadow file (staging artifact, not runtime source)
|
||||
try:
|
||||
CONFIG_PATH.write_text(
|
||||
json.dumps(config_data, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
logger.info("Wrote bot config: %s", CONFIG_PATH)
|
||||
logger.info("Wrote bot config shadow: %s", CONFIG_PATH)
|
||||
except OSError as e:
|
||||
logger.warning("Failed to write bot config: %s", e)
|
||||
return None
|
||||
logger.warning("Failed to write shadow config (non-fatal): %s", e)
|
||||
|
||||
# Step 5: Register in bot registry
|
||||
registered = register_bot(
|
||||
@@ -479,6 +485,7 @@ def create_bot(
|
||||
branch_name=branch_name,
|
||||
work_dir=RESOLVED_WORK_DIR,
|
||||
config_path=str(CONFIG_PATH),
|
||||
bot_token_ref=f"@api:telegram/{bot_id}",
|
||||
)
|
||||
if not registered:
|
||||
CONFIG_PATH.unlink(missing_ok=True)
|
||||
|
||||
@@ -792,3 +792,93 @@ class TestGetStatusAndGetAllBots:
|
||||
result = bot_operations.get_all_bots()
|
||||
assert result == expected
|
||||
mock_list_bots.assert_called_once()
|
||||
|
||||
|
||||
# =============================================
|
||||
# CREATE_BOT -> LOAD_BOT_CONFIG ROUND-TRIP
|
||||
# =============================================
|
||||
|
||||
|
||||
class TestCreateBotRoundTrip:
|
||||
"""Prove GAP1 is closed: create_bot persists config that load_bot_config reads."""
|
||||
|
||||
@patch("apps.handlers.bot_factory.start_bot_process", return_value=True)
|
||||
@patch("apps.handlers.bot_factory.enable_service", return_value=True)
|
||||
@patch("apps.handlers.bot_factory.set_bot_commands", return_value=True)
|
||||
@patch("apps.handlers.bot_factory.validate_token")
|
||||
@patch("apps.handlers.bot_factory.ensure_registry")
|
||||
def test_create_then_load_roundtrip(
|
||||
self,
|
||||
mock_ensure_registry,
|
||||
mock_validate_token,
|
||||
mock_set_commands,
|
||||
mock_enable,
|
||||
mock_start,
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
):
|
||||
"""After create_bot, load_bot_config returns the persisted config."""
|
||||
from apps.handlers import bot_factory, config as tg_config
|
||||
|
||||
mock_validate_token.return_value = {"username": "test_bot", "id": 123}
|
||||
|
||||
monkeypatch.setattr(bot_factory, "_BOT_CONFIG_DIR", tmp_path)
|
||||
|
||||
secrets_store = {}
|
||||
|
||||
def fake_set_secret(provider, slug, value, *, as_json=False):
|
||||
secrets_store[f"{provider}/{slug}"] = value
|
||||
return tmp_path / f"{slug}.json"
|
||||
|
||||
def fake_get_secret(provider, slug, *, as_json=False):
|
||||
return secrets_store.get(f"{provider}/{slug}")
|
||||
|
||||
monkeypatch.setattr(bot_factory, "_api_set_secret", fake_set_secret)
|
||||
monkeypatch.setattr(tg_config, "_api_get_secret", fake_get_secret)
|
||||
|
||||
monkeypatch.setattr("apps.handlers.bot_registry.REGISTRY_DIR", tmp_path / "state")
|
||||
monkeypatch.setattr(
|
||||
"apps.handlers.bot_registry.REGISTRY_FILE",
|
||||
tmp_path / "state" / "_registry.json",
|
||||
)
|
||||
|
||||
result = bot_factory.create_bot(
|
||||
bot_id="roundtrip_bot",
|
||||
bot_token="111:AAA-test-token",
|
||||
branch_name=None,
|
||||
allowed_user_ids=[42],
|
||||
)
|
||||
assert result is not None
|
||||
assert result["bot_id"] == "roundtrip_bot"
|
||||
|
||||
loaded = tg_config.load_bot_config("roundtrip_bot")
|
||||
assert loaded is not None
|
||||
assert loaded["bot_id"] == "roundtrip_bot"
|
||||
assert loaded["bot_token"] == "111:AAA-test-token"
|
||||
assert loaded["allowed_user_ids"] == [42]
|
||||
|
||||
@patch("apps.handlers.bot_factory.validate_token")
|
||||
@patch("apps.handlers.bot_factory.ensure_registry")
|
||||
def test_create_fails_loud_on_set_secret_error(
|
||||
self,
|
||||
mock_ensure_registry,
|
||||
mock_validate_token,
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
):
|
||||
"""create_bot returns None and logs error if set_secret raises."""
|
||||
from apps.handlers import bot_factory
|
||||
|
||||
mock_validate_token.return_value = {"username": "test_bot", "id": 123}
|
||||
monkeypatch.setattr(bot_factory, "_BOT_CONFIG_DIR", tmp_path)
|
||||
|
||||
def failing_set_secret(*args, **kwargs):
|
||||
raise OSError("permission denied")
|
||||
|
||||
monkeypatch.setattr(bot_factory, "_api_set_secret", failing_set_secret)
|
||||
|
||||
result = bot_factory.create_bot(
|
||||
bot_id="fail_bot",
|
||||
bot_token="222:BBB-test-token",
|
||||
)
|
||||
assert result is None
|
||||
|
||||
Reference in New Issue
Block a user