"""Helper script for tdd_e2e_implicit_init.robot smoke tests. Each subcommand exercises a real CLI command in a fresh environment where ``agents init`` has NOT been run, to reproduce bug #1023. Bug #1023 reports that ``CLEVERAGENTS_AUTO_APPLY_MIGRATIONS=true`` triggers schema migrations on an existing database but does NOT create the database file or its parent directory structure. As a result, any CLI command that touches the database fails with: sqlite3.OperationalError: unable to open database file The helper reports the **real** outcome: it exits 0 and prints the sentinel when the operation succeeds (bug is fixed), and exits 1 when the bug is still present. The ``tdd_expected_fail_listener`` on the Robot side handles pass/fail inversion while the bug remains open. Mock strategy ~~~~~~~~~~~~~ No mocks are needed — we exercise the real CLI via Typer's CliRunner in a pristine temporary directory. We only manipulate environment variables to create a fresh, uninitialised CLEVERAGENTS_HOME and set ``CLEVERAGENTS_AUTO_APPLY_MIGRATIONS=true``. """ from __future__ import annotations import os import shutil import sys import tempfile from collections.abc import Callable from pathlib import Path # Ensure local source tree is importable _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from typer.testing import CliRunner # noqa: E402 from cleveragents.application.container import reset_container # noqa: E402 from cleveragents.cli.main import app # noqa: E402 from cleveragents.config.settings import Settings # noqa: E402 runner = CliRunner() def _run_cli_without_init(args: list[str], sentinel: str) -> None: """Run a CLI command in a fresh environment without prior init. Creates a pristine temp directory, sets CLEVERAGENTS_HOME to it, enables CLEVERAGENTS_AUTO_APPLY_MIGRATIONS, and invokes the CLI. Exits 0 with sentinel on success; exits 1 on failure. Environment variables are always restored in the ``finally`` block so subsequent tests are not polluted even if the CLI invocation raises an unexpected exception. Exceptions are caught by CliRunner (the default behaviour) so that the result object always carries an ``exit_code`` and ``output``. Using ``catch_exceptions=False`` would cause non-AssertionError exceptions to propagate, preventing the ``tdd_expected_fail`` listener from inverting the result. Parameters ---------- args: CLI arguments to pass (e.g., ``["resource", "add", ...]``). sentinel: Sentinel string to print on success. """ tmpdir = tempfile.mkdtemp(prefix="tdd_implicit_init_robot_") env_save: dict[str, str | None] = {} try: # Save and remove env vars that point to pre-migrated databases, # template-DB fast-paths, or testing guards that auto-approve # migration prompts. CLEVERAGENTS_TEMPLATE_DB is particularly # important: when set, the Behave template-DB patch copies a # pre-migrated database, silently bypassing the directory- # creation bug this test is designed to capture. for key in ( "CLEVERAGENTS_AUTO_APPLY_MIGRATIONS", "CI", "BEHAVE_TESTING", "ROBOT_TESTING", "CLEVERAGENTS_HOME", "CLEVERAGENTS_DATABASE_URL", "CLEVERAGENTS_TEST_DATABASE_URL", "CLEVERAGENTS_TEMPLATE_DB", "CLEVERAGENTS_TESTING_USE_MOCK_AI", ): env_save[key] = os.environ.pop(key, None) os.environ["CLEVERAGENTS_HOME"] = tmpdir os.environ["CLEVERAGENTS_AUTO_APPLY_MIGRATIONS"] = "true" db_path = Path(tmpdir) / "cleveragents.db" test_db_path = Path(tmpdir) / "cleveragents_test.db" os.environ["CLEVERAGENTS_DATABASE_URL"] = f"sqlite:///{db_path}" os.environ["CLEVERAGENTS_TEST_DATABASE_URL"] = f"sqlite:///{test_db_path}" # Reset DI container so it picks up our env overrides. Settings.reset() reset_container() result = runner.invoke(app, args) if result.exit_code != 0: print( f"CLI command failed with exit code {result.exit_code}", file=sys.stderr, ) print(f"Output: {result.output}", file=sys.stderr) sys.exit(1) # Bug fixed — command succeeded without explicit init. print(sentinel) finally: # Always restore env vars and clean up the temp directory, # even if runner.invoke() or assertions raise. for key, val in env_save.items(): if val is not None: os.environ[key] = val else: os.environ.pop(key, None) Settings.reset() reset_container() shutil.rmtree(tmpdir, ignore_errors=True) # --------------------------------------------------------------------------- # Subcommands # --------------------------------------------------------------------------- def resource_add_no_init() -> None: """Invoke ``resource add`` without prior ``agents init``. Exercises the ``resource add git-checkout`` command in a fresh environment. If the database directory structure is not auto-created, this will fail with OperationalError. """ _run_cli_without_init( [ "resource", "add", "git-checkout", "local/tdd-test-resource", "--path", ".", "--branch", "main", ], "tdd-resource-add-no-init-ok", ) def project_create_no_init() -> None: """Invoke ``project create`` without prior ``agents init``. Exercises the ``project create`` command in a fresh environment. If the database directory structure is not auto-created, this will fail with OperationalError. """ _run_cli_without_init( ["project", "create", "local/tdd-test-project"], "tdd-project-create-no-init-ok", ) # --------------------------------------------------------------------------- # Dispatcher # --------------------------------------------------------------------------- _COMMANDS: dict[str, Callable[[], None]] = { "resource-add-no-init": resource_add_no_init, "project-create-no-init": project_create_no_init, } if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: print( f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr, ) sys.exit(1) cmd = _COMMANDS[sys.argv[1]] cmd()