From e2df239bd823cf06d531ab71c53183e26513ee39 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Fri, 10 Apr 2026 20:50:20 +0000 Subject: [PATCH 1/2] fix(cli): bootstrap registry database automatically Ensure tool and validation CLI commands initialize the SQLite persistence layer on first use and add regression coverage.\n\nISSUES CLOSED: #6885 --- .../steps/tdd_tool_cli_bootstrap_steps.py | 117 ++++++++++++++++++ features/tdd_tool_cli_bootstrap.feature | 17 +++ src/cleveragents/cli/bootstrap.py | 39 ++++++ src/cleveragents/cli/commands/tool.py | 4 + src/cleveragents/cli/commands/validation.py | 4 + 5 files changed, 181 insertions(+) create mode 100644 features/steps/tdd_tool_cli_bootstrap_steps.py create mode 100644 features/tdd_tool_cli_bootstrap.feature create mode 100644 src/cleveragents/cli/bootstrap.py diff --git a/features/steps/tdd_tool_cli_bootstrap_steps.py b/features/steps/tdd_tool_cli_bootstrap_steps.py new file mode 100644 index 000000000..dcc6eb4b6 --- /dev/null +++ b/features/steps/tdd_tool_cli_bootstrap_steps.py @@ -0,0 +1,117 @@ +"""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._instance = None # type: ignore[attr-defined] + + +@given("a CLI runner without a bootstrapped registry database") +def step_no_bootstrap(context) -> None: + context.runner = CliRunner() + + reset_container() + _reset_settings() + + cli_bootstrap._database_bootstrapped = False # type: ignore[attr-defined] + + 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._database_bootstrapped = False # type: ignore[attr-defined] + 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, ( + "Expected 'No tools found' in output.\n" + f"Actual 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." + ) diff --git a/features/tdd_tool_cli_bootstrap.feature b/features/tdd_tool_cli_bootstrap.feature new file mode 100644 index 000000000..988446556 --- /dev/null +++ b/features/tdd_tool_cli_bootstrap.feature @@ -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 diff --git a/src/cleveragents/cli/bootstrap.py b/src/cleveragents/cli/bootstrap.py new file mode 100644 index 000000000..9df0317e0 --- /dev/null +++ b/src/cleveragents/cli/bootstrap.py @@ -0,0 +1,39 @@ +"""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 + + +__all__ = ["ensure_cli_database_bootstrapped"] diff --git a/src/cleveragents/cli/commands/tool.py b/src/cleveragents/cli/commands/tool.py index 428082b35..df5137f38 100644 --- a/src/cleveragents/cli/commands/tool.py +++ b/src/cleveragents/cli/commands/tool.py @@ -51,6 +51,8 @@ from pathlib import Path from typing import Annotated, Any import typer + +from cleveragents.cli.bootstrap import ensure_cli_database_bootstrapped import yaml from rich.panel import Panel from rich.table import Table @@ -76,6 +78,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() diff --git a/src/cleveragents/cli/commands/validation.py b/src/cleveragents/cli/commands/validation.py index 4e6346c04..d0d5562d2 100644 --- a/src/cleveragents/cli/commands/validation.py +++ b/src/cleveragents/cli/commands/validation.py @@ -52,6 +52,8 @@ from pathlib import Path from typing import Annotated, Any import typer + +from cleveragents.cli.bootstrap import ensure_cli_database_bootstrapped import yaml from rich.console import Console from rich.panel import Panel @@ -81,6 +83,8 @@ def _get_tool_registry_service() -> Any: """ from cleveragents.application.container import get_container + ensure_cli_database_bootstrapped() + return get_container().tool_registry_service() -- 2.52.0 From dfad3de0a41ed70ef5a764a301aaf83adf827e17 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Mon, 13 Apr 2026 18:49:11 +0000 Subject: [PATCH 2/2] fix(cli): fix import ordering and remove type: ignore suppressions Resolve lint CI failure caused by un-sorted import blocks in tool.py and validation.py, and remove forbidden # type: ignore comments from the bootstrap step definitions. - Move first-party bootstrap import into the correct import group in cli/commands/tool.py and cli/commands/validation.py - Add reset_bootstrap_state() public helper to bootstrap.py for test-only use, replacing direct private attribute mutation - Replace Settings._instance = None # type: ignore with Settings.reset() - Replace cli_bootstrap._database_bootstrapped = False # type: ignore with cli_bootstrap.reset_bootstrap_state() ISSUES CLOSED: #6885 --- features/steps/tdd_tool_cli_bootstrap_steps.py | 9 ++++----- src/cleveragents/cli/bootstrap.py | 13 ++++++++++++- src/cleveragents/cli/commands/tool.py | 3 +-- src/cleveragents/cli/commands/validation.py | 3 +-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/features/steps/tdd_tool_cli_bootstrap_steps.py b/features/steps/tdd_tool_cli_bootstrap_steps.py index dcc6eb4b6..00381316c 100644 --- a/features/steps/tdd_tool_cli_bootstrap_steps.py +++ b/features/steps/tdd_tool_cli_bootstrap_steps.py @@ -20,7 +20,7 @@ from cleveragents.config.settings import Settings def _reset_settings() -> None: """Reset singleton settings between scenarios.""" - Settings._instance = None # type: ignore[attr-defined] + Settings.reset() @given("a CLI runner without a bootstrapped registry database") @@ -30,7 +30,7 @@ def step_no_bootstrap(context) -> None: reset_container() _reset_settings() - cli_bootstrap._database_bootstrapped = False # type: ignore[attr-defined] + cli_bootstrap.reset_bootstrap_state() tmpdir = tempfile.mkdtemp(prefix="tdd_tool_cli_bootstrap_6885_") db_path = Path(tmpdir) / "registry.db" @@ -42,7 +42,7 @@ def step_no_bootstrap(context) -> None: def _cleanup() -> None: os.environ.pop("CLEVERAGENTS_DATABASE_URL", None) - cli_bootstrap._database_bootstrapped = False # type: ignore[attr-defined] + cli_bootstrap.reset_bootstrap_state() reset_container() _reset_settings() shutil.rmtree(tmpdir, ignore_errors=True) @@ -104,8 +104,7 @@ def step_validation_add_exit_ok(context) -> None: def step_tool_list_output(context) -> None: output = context.result.output assert "No tools found" in output, ( - "Expected 'No tools found' in output.\n" - f"Actual output:\n{output}" + f"Expected 'No tools found' in output.\nActual output:\n{output}" ) diff --git a/src/cleveragents/cli/bootstrap.py b/src/cleveragents/cli/bootstrap.py index 9df0317e0..8db99f921 100644 --- a/src/cleveragents/cli/bootstrap.py +++ b/src/cleveragents/cli/bootstrap.py @@ -36,4 +36,15 @@ def ensure_cli_database_bootstrapped(force: bool = False) -> None: _database_bootstrapped = True -__all__ = ["ensure_cli_database_bootstrapped"] +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"] diff --git a/src/cleveragents/cli/commands/tool.py b/src/cleveragents/cli/commands/tool.py index df5137f38..9c46b5c99 100644 --- a/src/cleveragents/cli/commands/tool.py +++ b/src/cleveragents/cli/commands/tool.py @@ -51,12 +51,11 @@ from pathlib import Path from typing import Annotated, Any import typer - -from cleveragents.cli.bootstrap import ensure_cli_database_bootstrapped 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 ( diff --git a/src/cleveragents/cli/commands/validation.py b/src/cleveragents/cli/commands/validation.py index d0d5562d2..eb2bb65c1 100644 --- a/src/cleveragents/cli/commands/validation.py +++ b/src/cleveragents/cli/commands/validation.py @@ -52,12 +52,11 @@ from pathlib import Path from typing import Annotated, Any import typer - -from cleveragents.cli.bootstrap import ensure_cli_database_bootstrapped 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, -- 2.52.0