From dc1ecaab47e417ea5979d5e6c10e2c584a2cf366 Mon Sep 17 00:00:00 2001 From: "Brent E. Edwards" Date: Fri, 6 Mar 2026 20:44:36 +0000 Subject: [PATCH] test(resource): add failing tests for built-in fs-directory type bootstrap Add TDD-style Behave BDD tests for the built-in fs-directory resource type bootstrap (bug #523). Three Gherkin scenarios: one failing TDD test reproducing the bug (no bootstrap called during init, tagged @wip), and two regression tests verifying bootstrap_builtin_types() seeds correct data and resource add fs-directory succeeds after bootstrap. Includes Robot Framework regression tests. Review feedback addressed: - Removed all 21 unnecessary # type: ignore comments (hurui200320 M1) - Fixed is not True to is False for clarity (Aditya F2) - Fixed Robot common.resource path to ${CURDIR}/common.resource (hurui200320 L1) - Squashed all commits into one and rebased onto master (C1, C2) - Added CHANGELOG entry with correct scenario count Closes #537 --- CHANGELOG.md | 5 + features/resource_type_bootstrap_fs.feature | 44 ++++ .../steps/resource_type_bootstrap_fs_steps.py | 207 ++++++++++++++++++ robot/resource_type_bootstrap_fs.robot | 63 ++++++ 4 files changed, 319 insertions(+) create mode 100644 features/resource_type_bootstrap_fs.feature create mode 100644 features/steps/resource_type_bootstrap_fs_steps.py create mode 100644 robot/resource_type_bootstrap_fs.robot diff --git a/CHANGELOG.md b/CHANGELOG.md index e2bb52577..c7ceceedc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Added TDD-style Behave BDD tests for the built-in `fs-directory` resource type + bootstrap. Three scenarios: one failing TDD test reproducing bug #523 (no bootstrap + called during init), and two regression tests verifying `bootstrap_builtin_types()` + seeds correct data and `agents resource add fs-directory` succeeds. Includes Robot + Framework regression tests. (#537) - Added general-purpose domain event system under `cleveragents.infrastructure.events`. `EventType` StrEnum defines 38 typed event identifiers across 9 domains (plan lifecycle, decision, invariant, actor, diff --git a/features/resource_type_bootstrap_fs.feature b/features/resource_type_bootstrap_fs.feature new file mode 100644 index 000000000..7cafc59d4 --- /dev/null +++ b/features/resource_type_bootstrap_fs.feature @@ -0,0 +1,44 @@ +# These tests target bug #523 and are expected to fail until the fix is applied. +# bootstrap_builtin_types() is never called during initialization, so the +# built-in fs-directory resource type is not seeded into the database. + +Feature: Built-in fs-directory Resource Type Bootstrap + As a CleverAgents user + I want the built-in fs-directory resource type to be available after initialization + So that I can run "agents resource add fs-directory" without "Resource type not found" + + # ── Bug reproduction: init path does NOT call bootstrap ──── + # This scenario is the TDD failing test. It creates a registry WITHOUT + # calling bootstrap_builtin_types() and asserts fs-directory exists. + # It will fail until the fix integrates bootstrap into the init path. + # + # NOTE FOR FIX AUTHOR: If the chosen fix is to call bootstrap inside + # init_command() / initialize_project() rather than in + # ResourceRegistryService.__init__(), you will need to update the Given + # step to exercise the init path instead of constructing a bare service. + + @tdd @bug523 @wip + Scenario: fs-directory type exists after init without explicit bootstrap call + Given a fresh in-memory resource registry without bootstrap + When I query the fs bootstrap resource type registry for "fs-directory" + Then the bootstrap fs resource type "fs-directory" should exist + + # ── Regression: bootstrap function itself works correctly ── + + @tdd @bug523 + Scenario: After initialization fs-directory type exists in the registry + Given a fresh in-memory resource registry with bootstrap + When I query the fs bootstrap resource type registry for "fs-directory" + Then the bootstrap fs resource type "fs-directory" should exist + And the bootstrap fs resource type kind should be "physical" + And the bootstrap fs resource type sandbox_strategy should be "copy_on_write" + + # ── CLI add command ──────────────────────────────────────── + + @tdd @bug523 + Scenario: resource add fs-directory succeeds after bootstrap + Given a fresh in-memory resource registry with bootstrap + When I run resource add for type "fs-directory" named "local/test" with path "/tmp/test" + Then the resource add command should succeed + And the resource add output should contain "Added resource" + And the resource add output should not contain "Resource type not found" diff --git a/features/steps/resource_type_bootstrap_fs_steps.py b/features/steps/resource_type_bootstrap_fs_steps.py new file mode 100644 index 000000000..2e46cd16a --- /dev/null +++ b/features/steps/resource_type_bootstrap_fs_steps.py @@ -0,0 +1,207 @@ +"""Step definitions for resource_type_bootstrap_fs.feature. + +These tests target bug #523. The first scenario (without bootstrap) is a +genuine TDD failing test: it creates a fresh registry *without* calling +``bootstrap_builtin_types()`` and asserts ``fs-directory`` exists — which +fails until the init path is fixed to call bootstrap. + +The remaining scenarios verify that ``bootstrap_builtin_types()`` itself +seeds the correct data and that the CLI ``resource add`` path works once +the types are present. +""" + +from __future__ import annotations + +from io import StringIO +from typing import Any +from unittest.mock import patch + +from behave import given, then, when +from behave.runner import Context +from rich.console import Console +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, +) +from cleveragents.core.exceptions import NotFoundError +from cleveragents.infrastructure.database.models import Base + +# ── Helpers ────────────────────────────────────────────────── + +_PATCH_SERVICE = "cleveragents.cli.commands.resource._get_registry_service" +_PATCH_CONSOLE = "cleveragents.cli.commands.resource.console" + + +def _make_service(context: Context, *, run_bootstrap: bool) -> ResourceRegistryService: + """Create an in-memory ResourceRegistryService. + + When *run_bootstrap* is ``True`` the built-in types are seeded; when + ``False`` the registry is left empty (reproducing the bug). + """ + engine = create_engine("sqlite:///:memory:", echo=False) + Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, expire_on_commit=False) + service = ResourceRegistryService(session_factory=factory) + if run_bootstrap: + service.bootstrap_builtin_types() + context.bootstrap_fs_service = service + return service + + +def _capture_output(func: Any, *args: Any, **kwargs: Any) -> tuple[str, bool]: + """Run a CLI function capturing its console output and success status.""" + buf = StringIO() + console = Console( + file=buf, width=200, no_color=True, highlight=False, force_terminal=False + ) + + failed = False + failure_reason = "" + with patch(_PATCH_CONSOLE, console): + try: + func(*args, **kwargs) + except SystemExit as exc: + # Defensive: currently resource_add raises typer.Abort (a + # RuntimeError subclass) on failure, not SystemExit. This + # handler guards against future changes where the function + # is invoked through the Typer CLI runner, which converts + # exceptions into SystemExit. + if exc.code not in (None, 0): + failed = True + failure_reason = f"SystemExit: {exc.code}" + except Exception as exc: + failed = True + # typer.Abort() carries no message; str(exc) is empty. + # Fall back to the class name so failure_reason is not blank. + reason = str(exc) if str(exc) else "(no message)" + failure_reason = f"{type(exc).__name__}: {reason}" + + output = buf.getvalue() + if failure_reason: + output = output + "\n" + failure_reason + return output, failed + + +# ── Given steps ────────────────────────────────────────────── + + +@given("a fresh in-memory resource registry with bootstrap") +def step_fresh_registry_with_bootstrap(context: Context) -> None: + """Set up a fresh in-memory database and run bootstrap_builtin_types.""" + _make_service(context, run_bootstrap=True) + context.bootstrap_fs_output = "" + context.bootstrap_fs_failed = False + + +@given("a fresh in-memory resource registry without bootstrap") +def step_fresh_registry_without_bootstrap(context: Context) -> None: + """Set up a fresh in-memory database WITHOUT running bootstrap. + + This reproduces bug #523: the registry is empty because + ``bootstrap_builtin_types()`` is never called during initialization. + """ + _make_service(context, run_bootstrap=False) + context.bootstrap_fs_output = "" + context.bootstrap_fs_failed = False + + +# ── When steps ─────────────────────────────────────────────── + + +@when('I query the fs bootstrap resource type registry for "{name}"') +def step_query_registry_for_type(context: Context, name: str) -> None: + """Query the registry for a specific resource type by name.""" + service: ResourceRegistryService = context.bootstrap_fs_service + try: + context.bootstrap_fs_type_spec = service.show_type(name) + context.bootstrap_fs_type_found = True + except NotFoundError: + context.bootstrap_fs_type_spec = None + context.bootstrap_fs_type_found = False + + +@when('I run resource add for type "{type_name}" named "{name}" with path "{path}"') +def step_run_resource_add_fs( + context: Context, type_name: str, name: str, path: str +) -> None: + """Run resource add command via the CLI function.""" + from cleveragents.cli.commands.resource import resource_add + + with patch( + _PATCH_SERVICE, + return_value=context.bootstrap_fs_service, + ): + output, failed = _capture_output( + resource_add, + type_name=type_name, + name=name, + path=path, + branch=None, + description=None, + image=None, + read_only=False, + fmt="rich", + ) + context.bootstrap_fs_output = output + context.bootstrap_fs_failed = failed + + +# ── Then steps ─────────────────────────────────────────────── + + +@then('the bootstrap fs resource type "{name}" should exist') +def step_type_should_exist(context: Context, name: str) -> None: + """Assert the resource type was found in the registry.""" + assert context.bootstrap_fs_type_found is True, ( + f"Expected resource type '{name}' to exist in the registry after " + f"bootstrap, but it was not found." + ) + + +@then('the bootstrap fs resource type kind should be "{kind}"') +def step_type_kind(context: Context, kind: str) -> None: + """Assert the resource type has the expected kind.""" + spec = context.bootstrap_fs_type_spec + actual = spec.resource_kind + actual_str = actual.value if hasattr(actual, "value") else str(actual) + assert actual_str == kind, f"Expected kind '{kind}', got '{actual_str}'" + + +@then('the bootstrap fs resource type sandbox_strategy should be "{strategy}"') +def step_type_sandbox(context: Context, strategy: str) -> None: + """Assert the resource type has the expected sandbox strategy.""" + spec = context.bootstrap_fs_type_spec + actual = spec.sandbox_strategy + actual_str = actual.value if hasattr(actual, "value") else str(actual) + assert actual_str == strategy, ( + f"Expected sandbox_strategy '{strategy}', got '{actual_str}'" + ) + + +@then("the resource add command should succeed") +def step_resource_add_should_succeed(context: Context) -> None: + """Assert the resource add command did not fail.""" + assert context.bootstrap_fs_failed is False, ( + f"Expected 'resource add' to succeed, but it failed. " + f"Output: {context.bootstrap_fs_output!r}" + ) + + +@then('the resource add output should contain "{text}"') +def step_fs_output_should_contain(context: Context, text: str) -> None: + """Assert the output contains the given text.""" + assert text in context.bootstrap_fs_output, ( + f"Expected output to contain '{text}', but it did not. " + f"Full output: {context.bootstrap_fs_output!r}" + ) + + +@then('the resource add output should not contain "{text}"') +def step_output_should_not_contain(context: Context, text: str) -> None: + """Assert the output does not contain the given text.""" + assert text not in context.bootstrap_fs_output, ( + f"Output unexpectedly contained '{text}'. " + f"Full output: {context.bootstrap_fs_output!r}" + ) diff --git a/robot/resource_type_bootstrap_fs.robot b/robot/resource_type_bootstrap_fs.robot new file mode 100644 index 000000000..826f88117 --- /dev/null +++ b/robot/resource_type_bootstrap_fs.robot @@ -0,0 +1,63 @@ +*** Settings *** +Documentation Regression tests for built-in fs-directory type bootstrap (bug #523). +... These tests verify that bootstrap_builtin_types() correctly seeds +... fs-directory into the registry and that resource registration succeeds +... after bootstrap. Both tests call bootstrap explicitly and should pass. +Library Process +Library OperatingSystem +Resource ${CURDIR}/common.resource + +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Test Cases *** +Bootstrap Seeds Fs Directory Type Into Registry + [Documentation] Verify that bootstrap_builtin_types() seeds fs-directory into the + ... database so that show_type("fs-directory") succeeds. + ${script}= Catenate SEPARATOR=\n + ... from sqlalchemy import create_engine + ... from sqlalchemy.orm import sessionmaker + ... from cleveragents.infrastructure.database.models import Base + ... from cleveragents.application.services.resource_registry_service import ResourceRegistryService + ... engine = create_engine("sqlite:///:memory:", echo=False) + ... Base.metadata.create_all(engine) + ... factory = sessionmaker(bind=engine, expire_on_commit=False) + ... service = ResourceRegistryService(session_factory=factory) + ... registered = service.bootstrap_builtin_types() + ... assert "fs-directory" in registered, f"fs-directory not in registered: {registered}" + ... spec = service.show_type("fs-directory") + ... assert spec.name == "fs-directory", f"name mismatch: {spec.name}" + ... kind = spec.resource_kind.value if hasattr(spec.resource_kind, "value") else str(spec.resource_kind) + ... assert kind == "physical", f"kind: {kind}" + ... strategy = spec.sandbox_strategy.value if hasattr(spec.sandbox_strategy, "value") else str(spec.sandbox_strategy) + ... assert strategy == "copy_on_write", f"strategy: {strategy}" + ... print("fs-directory bootstrap validated successfully") + ${result}= Run Process ${PYTHON} -c ${script} timeout=60s + Should Be Equal As Integers ${result.rc} 0 fs-directory bootstrap failed: ${result.stderr} + Should Contain ${result.stdout} fs-directory bootstrap validated successfully + +Resource Add Fs Directory Succeeds After Bootstrap + [Documentation] Verify that after bootstrap, registering an fs-directory resource + ... instance succeeds (no "Resource type not found" error). + ${script}= Catenate SEPARATOR=\n + ... from sqlalchemy import create_engine + ... from sqlalchemy.orm import sessionmaker + ... from cleveragents.infrastructure.database.models import Base + ... from cleveragents.application.services.resource_registry_service import ResourceRegistryService + ... engine = create_engine("sqlite:///:memory:", echo=False) + ... Base.metadata.create_all(engine) + ... factory = sessionmaker(bind=engine, expire_on_commit=False) + ... service = ResourceRegistryService(session_factory=factory) + ... service.bootstrap_builtin_types() + ... resource = service.register_resource( + ... ${SPACE}${SPACE}${SPACE}${SPACE}type_name="fs-directory", + ... ${SPACE}${SPACE}${SPACE}${SPACE}name="local/test", + ... ${SPACE}${SPACE}${SPACE}${SPACE}location="/tmp/test", + ... ${SPACE}${SPACE}${SPACE}${SPACE}description="Test fs-directory resource", + ... ) + ... assert resource.resource_type_name == "fs-directory", f"type: {resource.resource_type_name}" + ... assert resource.name == "local/test", f"name: {resource.name}" + ... print("resource add fs-directory succeeded after bootstrap") + ${result}= Run Process ${PYTHON} -c ${script} timeout=60s + Should Be Equal As Integers ${result.rc} 0 resource add fs-directory failed: ${result.stderr} + Should Contain ${result.stdout} resource add fs-directory succeeded after bootstrap -- 2.52.0