fix(cli): bootstrap registry database automatically #7510

Merged
HAL9000 merged 4 commits from bugfix/6885-tool-cli-bootstrap into master 2026-04-19 23:04:04 +00:00
5 changed files with 189 additions and 0 deletions
@@ -0,0 +1,116 @@
"""Step definitions for TDD Issue #6885 — CLI registry bootstrap."""
from __future__ import annotations
import os
import shutil
import tempfile
from pathlib import Path
from behave import given, then, when
from typer.testing import CliRunner
import cleveragents.cli.bootstrap as cli_bootstrap
from cleveragents.application.container import reset_container
from cleveragents.cli.commands.tool import app as tool_app
from cleveragents.cli.commands.validation import app as validation_app
from cleveragents.config.settings import Settings
def _reset_settings() -> None:
"""Reset singleton settings between scenarios."""
Settings.reset()
@given("a CLI runner without a bootstrapped registry database")
def step_no_bootstrap(context) -> None:
context.runner = CliRunner()
reset_container()
_reset_settings()
cli_bootstrap.reset_bootstrap_state()
tmpdir = tempfile.mkdtemp(prefix="tdd_tool_cli_bootstrap_6885_")
db_path = Path(tmpdir) / "registry.db"
context._tool_cli_tmpdir = tmpdir
context._tool_cli_db_path = db_path
os.environ["CLEVERAGENTS_DATABASE_URL"] = f"sqlite:///{db_path}"
def _cleanup() -> None:
os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
cli_bootstrap.reset_bootstrap_state()
reset_container()
_reset_settings()
shutil.rmtree(tmpdir, ignore_errors=True)
context.add_cleanup(_cleanup)
@when("I invoke tool list without prior bootstrap")
def step_invoke_tool_list(context) -> None:
context.result = context.runner.invoke(tool_app, ["list"])
@when("I invoke validation add without prior bootstrap")
def step_invoke_validation_add(context) -> None:
config_path = Path(context._tool_cli_tmpdir) / "validation.yaml"
config_path.write_text(
"""
name: local/test-validation
description: temporary validation for TDD issue 6885
source: custom
mode: informational
code: |
def run(inputs):
return {"passed": True}
""".strip()
)
context.result = context.runner.invoke(
validation_app,
[
"add",
"--config",
str(config_path),
"--format",
"json",
],
)
@then("the tool list command should exit successfully")
def step_tool_list_exit_ok(context) -> None:
assert context.result.exit_code == 0, (
f"Expected exit code 0, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}\n"
f"Exception: {getattr(context.result, 'exception', None)!r}"
)
@then("the validation add command should exit successfully")
def step_validation_add_exit_ok(context) -> None:
assert context.result.exit_code == 0, (
f"Expected exit code 0, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}\n"
f"Exception: {getattr(context.result, 'exception', None)!r}"
)
@then("the tool list output should indicate that no tools are registered")
def step_tool_list_output(context) -> None:
output = context.result.output
assert "No tools found" in output, (
f"Expected 'No tools found' in output.\nActual output:\n{output}"
)
@then("the validation add output should report the registered validation in JSON")
def step_validation_add_output(context) -> None:
output = context.result.output
assert '"name": "local/test-validation"' in output, (
"Expected the registered validation name in the JSON output."
)
+17
View File
@@ -0,0 +1,17 @@
@tdd_issue @tdd_issue_6885
Feature: TDD Issue #6885 — Tool CLI bootstraps database automatically
As a developer
I want `agents tool list` and `agents validation add` to work on a fresh install
So that users do not have to run a manual database upgrade before using the registry
Scenario: Tool list command bootstraps the database automatically
Given a CLI runner without a bootstrapped registry database
When I invoke tool list without prior bootstrap
Then the tool list command should exit successfully
And the tool list output should indicate that no tools are registered
Scenario: Validation add command bootstraps the database automatically
Given a CLI runner without a bootstrapped registry database
When I invoke validation add without prior bootstrap
Then the validation add command should exit successfully
And the validation add output should report the registered validation in JSON
+50
View File
@@ -0,0 +1,50 @@
"""CLI bootstrap helpers.
Ensures process-wide initialization for CLI commands that depend on
persistence-backed registries by running Alembic migrations exactly once per
process.
"""
from __future__ import annotations
from threading import Lock
_database_bootstrapped = False
_bootstrap_lock = Lock()
def ensure_cli_database_bootstrapped(force: bool = False) -> None:
"""Ensure CLI database schema exists and migrations are applied."""
global _database_bootstrapped
if _database_bootstrapped and not force:
return
with _bootstrap_lock:
if _database_bootstrapped and not force:
return
from cleveragents.application.container import get_database_url
from cleveragents.infrastructure.database.migration_runner import (
MigrationRunner,
)
runner = MigrationRunner(get_database_url())
runner.init_or_upgrade(require_confirmation=False)
_database_bootstrapped = True
def reset_bootstrap_state() -> None:
"""Reset the bootstrap state flag.
.. warning:: **Test use only.** Do not call in production code paths —
resetting bootstrap state mid-flight can cause inconsistent database
initialisation state.
"""
global _database_bootstrapped
_database_bootstrapped = False
__all__ = ["ensure_cli_database_bootstrapped", "reset_bootstrap_state"]
+3
View File
@@ -55,6 +55,7 @@ import yaml
from rich.panel import Panel
from rich.table import Table
from cleveragents.cli.bootstrap import ensure_cli_database_bootstrapped
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console
from cleveragents.core.exceptions import (
@@ -76,6 +77,8 @@ def _get_tool_registry_service() -> Any:
"""Get the ToolRegistryService from the container."""
from cleveragents.application.container import get_container
ensure_cli_database_bootstrapped()
container = get_container()
database_url: str = container.database_url()
@@ -56,6 +56,7 @@ import yaml
from rich.console import Console
from rich.panel import Panel
from cleveragents.cli.bootstrap import ensure_cli_database_bootstrapped
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.core.exceptions import (
CleverAgentsError,
@@ -81,6 +82,8 @@ def _get_tool_registry_service() -> Any:
"""
from cleveragents.application.container import get_container
ensure_cli_database_bootstrapped()
return get_container().tool_registry_service()