fix(cli): bootstrap registry database automatically #7510
@@ -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."
|
||||
)
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user