diff --git a/CHANGELOG.md b/CHANGELOG.md index aeeb219a..ccfb536b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/aipass/skills/lib/telegram/apps/handlers/bot_factory.py b/src/aipass/skills/lib/telegram/apps/handlers/bot_factory.py index be0b3365..0d8292f9 100644 --- a/src/aipass/skills/lib/telegram/apps/handlers/bot_factory.py +++ b/src/aipass/skills/lib/telegram/apps/handlers/bot_factory.py @@ -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) diff --git a/src/aipass/skills/lib/telegram/tests/test_multibot_config.py b/src/aipass/skills/lib/telegram/tests/test_multibot_config.py index a2197524..f0356ad3 100644 --- a/src/aipass/skills/lib/telegram/tests/test_multibot_config.py +++ b/src/aipass/skills/lib/telegram/tests/test_multibot_config.py @@ -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