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:
AIOSAI
2026-06-24 16:49:06 -07:00
co-authored by Claude Opus 4.8
parent 0096aef1d2
commit 9af4c8ac05
3 changed files with 111 additions and 8 deletions
+6
View File
@@ -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