From 23c1678058571fa141e150d048d460cd35a1c256 Mon Sep 17 00:00:00 2001 From: "Brent E. Edwards" Date: Wed, 4 Mar 2026 20:13:36 +0000 Subject: [PATCH 1/7] test(resource): add failing tests for built-in git-checkout type bootstrap Add TDD-style failing tests that verify the built-in git-checkout resource type is available after initialization. Tests assert the correct expected behavior: after agents init, the git-checkout type should exist in the registry and 'agents resource add git-checkout' should succeed. Tests are expected to fail until bug #524 is fixed, because bootstrap_builtin_types() is never called during initialization. The fix branch should be based on this branch so the fix commit inherits these tests. Files added: - features/resource_type_bootstrap_git.feature (2 Behave scenarios) - features/steps/resource_type_bootstrap_git_steps.py (step definitions) - robot/resource_type_bootstrap_git.robot (Robot Framework smoke test) ISSUES CLOSED: #553 --- features/resource_type_bootstrap_git.feature | 26 +++ .../resource_type_bootstrap_git_steps.py | 167 ++++++++++++++++++ robot/resource_type_bootstrap_git.robot | 50 ++++++ 3 files changed, 243 insertions(+) create mode 100644 features/resource_type_bootstrap_git.feature create mode 100644 features/steps/resource_type_bootstrap_git_steps.py create mode 100644 robot/resource_type_bootstrap_git.robot diff --git a/features/resource_type_bootstrap_git.feature b/features/resource_type_bootstrap_git.feature new file mode 100644 index 000000000..3b52913fc --- /dev/null +++ b/features/resource_type_bootstrap_git.feature @@ -0,0 +1,26 @@ +# These tests target bug #524 and are expected to fail until the fix is applied. +# bootstrap_builtin_types() is never called during initialization, so the +# git-checkout built-in resource type is missing from the registry at runtime. +Feature: Built-in git-checkout type bootstrap on initialization + As a CleverAgents user + I want the built-in git-checkout resource type to be available after initialization + So that I can run "agents resource add git-checkout" without "Resource type not found" + + # ── Registry presence after bootstrap ────────────────────── + + Scenario: After initialization the git-checkout type exists in the resource type registry + Given a freshly initialised resource registry service + When bootstrap_builtin_types is called during initialisation + Then the resource type "git-checkout" should be present in the registry + And the resource type "git-checkout" should have kind "physical" + And the resource type "git-checkout" should have sandbox_strategy "git_worktree" + And the resource type "git-checkout" should be user_addable + + # ── CLI resource add succeeds ────────────────────────────── + + Scenario: agents resource add git-checkout succeeds without Resource type not found error + Given a freshly initialised resource registry service + And bootstrap_builtin_types is called during initialisation + When I run "agents resource add git-checkout local/test --path /tmp/repo --branch main" + Then the CLI exit code should be 0 + And the CLI output should not contain "Resource type not found" diff --git a/features/steps/resource_type_bootstrap_git_steps.py b/features/steps/resource_type_bootstrap_git_steps.py new file mode 100644 index 000000000..fbf20015e --- /dev/null +++ b/features/steps/resource_type_bootstrap_git_steps.py @@ -0,0 +1,167 @@ +"""Step definitions for resource_type_bootstrap_git.feature. + +These tests target bug #524 and are expected to fail until the fix is applied. +bootstrap_builtin_types() is never called during initialization, so the +git-checkout built-in resource type is missing from the registry at runtime. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from behave import given, then, when # type: ignore[import-untyped] +from behave.runner import Context # type: ignore[import-untyped] +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from typer.testing import CliRunner + +from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, +) +from cleveragents.infrastructure.database.models import Base + +runner = CliRunner() + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _fresh_service() -> ResourceRegistryService: + """Create a ResourceRegistryService backed by an in-memory SQLite DB.""" + engine = create_engine("sqlite:///:memory:", echo=False) + Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, expire_on_commit=False) + return ResourceRegistryService(session_factory=factory) + + +# --------------------------------------------------------------------------- +# Given steps +# --------------------------------------------------------------------------- + + +@given("a freshly initialised resource registry service") +def step_fresh_registry(context: Context) -> None: + """Create a fresh in-memory registry service (no bootstrap yet).""" + context.bootstrap_service = _fresh_service() # type: ignore[attr-defined] + + +# --------------------------------------------------------------------------- +# When steps +# --------------------------------------------------------------------------- + + +@when("bootstrap_builtin_types is called during initialisation") +def step_call_bootstrap(context: Context) -> None: + """Call bootstrap_builtin_types, which should seed built-in types.""" + service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] + context.bootstrap_registered = service.bootstrap_builtin_types() # type: ignore[attr-defined] + + +@given("bootstrap_builtin_types is called during initialisation") +def step_given_bootstrap(context: Context) -> None: + """Call bootstrap as a precondition (Given variant).""" + step_call_bootstrap(context) + + +@when( + 'I run "agents resource add git-checkout local/test --path /tmp/repo --branch main"' +) +def step_run_resource_add_cli(context: Context) -> None: + """Invoke the resource add CLI command via CliRunner with mocked DI.""" + from cleveragents.cli.commands.resource import app as resource_app + + service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] + + mock_container = MagicMock() + mock_container.resource_registry_service.return_value = service + + with patch( + "cleveragents.cli.commands.resource.get_container", + return_value=mock_container, + ): + result = runner.invoke( + resource_app, + [ + "add", + "git-checkout", + "local/test", + "--path", + "/tmp/repo", + "--branch", + "main", + ], + ) + + context.cli_result = result # type: ignore[attr-defined] + + +# --------------------------------------------------------------------------- +# Then steps +# --------------------------------------------------------------------------- + + +@then('the resource type "{name}" should be present in the registry') +def step_type_present(context: Context, name: str) -> None: + """Assert the named resource type exists in the registry.""" + service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] + all_types = service.list_types() + type_names = [t.name for t in all_types] + assert name in type_names, f"Expected '{name}' in registry, got: {type_names}" + + +@then('the resource type "{name}" should have kind "{kind}"') +def step_type_kind(context: Context, name: str, kind: str) -> None: + """Assert the resource type has the expected resource_kind.""" + service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] + spec = service.show_type(name) + actual = ( + spec.resource_kind.value + if hasattr(spec.resource_kind, "value") + else str(spec.resource_kind) + ) + assert actual == kind, f"Expected kind '{kind}', got '{actual}'" + + +@then('the resource type "{name}" should have sandbox_strategy "{strategy}"') +def step_type_sandbox(context: Context, name: str, strategy: str) -> None: + """Assert the resource type has the expected sandbox_strategy.""" + service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] + spec = service.show_type(name) + actual = ( + spec.sandbox_strategy.value + if hasattr(spec.sandbox_strategy, "value") + else str(spec.sandbox_strategy) + ) + assert actual == strategy, f"Expected strategy '{strategy}', got '{actual}'" + + +@then('the resource type "{name}" should be user_addable') +def step_type_user_addable(context: Context, name: str) -> None: + """Assert the resource type is user-addable.""" + service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] + spec = service.show_type(name) + assert spec.user_addable is True, ( + f"Expected user_addable=True, got {spec.user_addable}" + ) + + +@then("the CLI exit code should be {code:d}") +def step_cli_exit_code(context: Context, code: int) -> None: + """Assert the CLI runner result exit code.""" + result = context.cli_result # type: ignore[attr-defined] + assert result.exit_code == code, ( + f"Expected exit code {code}, got {result.exit_code}.\n" + f"Output: {result.output}\n" + f"Exception: {result.exception!r}" + ) + + +@then('the CLI output should not contain "{text}"') +def step_cli_no_text(context: Context, text: str) -> None: + """Assert the CLI output does not contain the given text.""" + result = context.cli_result # type: ignore[attr-defined] + assert text not in result.output, ( + f"Did not expect '{text}' in CLI output, but found it:\n{result.output}" + ) diff --git a/robot/resource_type_bootstrap_git.robot b/robot/resource_type_bootstrap_git.robot new file mode 100644 index 000000000..67d7f3db6 --- /dev/null +++ b/robot/resource_type_bootstrap_git.robot @@ -0,0 +1,50 @@ +*** Settings *** +Documentation Integration smoke tests for built-in git-checkout type bootstrap (bug #524). +... These tests are expected to FAIL until the fix is applied because +... bootstrap_builtin_types() is never called during initialization. +Library Process +Library OperatingSystem +Resource common.resource + +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Test Cases *** +Resource Add Git Checkout Should Not Fail With Type Not Found + [Documentation] Run "agents resource add git-checkout" and verify it does not + ... produce a "Resource type not found" error. This currently fails + ... because bootstrap_builtin_types() is never called. + ${result}= Run Process ${PYTHON} -m cleveragents resource add + ... git-checkout local/test --path /tmp/repo --branch main + ... timeout=60s + Should Not Contain ${result.stdout} Resource type not found + Should Not Contain ${result.stderr} Resource type not found + Should Be Equal As Integers ${result.rc} 0 + ... msg=Expected exit code 0 but got ${result.rc}. stdout: ${result.stdout} stderr: ${result.stderr} + +Git Checkout Type Exists After Bootstrap + [Documentation] Directly call bootstrap_builtin_types() and verify git-checkout + ... is present in the type registry. + ${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 "git-checkout" in registered, f"git-checkout not in registered: {registered}" + ... spec = service.show_type("git-checkout") + ... assert spec.name == "git-checkout", f"name mismatch: {spec.name}" + ... rk = spec.resource_kind.value if hasattr(spec.resource_kind, "value") else str(spec.resource_kind) + ... assert rk == "physical", f"kind mismatch: {rk}" + ... ss = spec.sandbox_strategy.value if hasattr(spec.sandbox_strategy, "value") else str(spec.sandbox_strategy) + ... assert ss == "git_worktree", f"sandbox mismatch: {ss}" + ... assert spec.user_addable is True, f"user_addable: {spec.user_addable}" + ... print("git-checkout bootstrap validated successfully") + ${result}= Run Process ${PYTHON} -c ${script} timeout=60s + Should Be Equal As Integers ${result.rc} 0 + ... msg=Bootstrap validation failed: ${result.stderr} + Should Contain ${result.stdout} git-checkout bootstrap validated successfully -- 2.52.0 From dbf324352a0323e82d9254223f6319836ae98963 Mon Sep 17 00:00:00 2001 From: "Brent E. Edwards" Date: Wed, 4 Mar 2026 20:19:27 +0000 Subject: [PATCH 2/7] docs(changelog): add entry for git-checkout bootstrap test scaffolding ISSUES CLOSED: #553 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ceceedc..512ed058c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ 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 TDD-style failing Behave BDD tests for the built-in `git-checkout` resource type + bootstrap. Two scenarios verify that `git-checkout` exists in the registry after init and + that `agents resource add git-checkout` succeeds. Includes Robot Framework smoke tests. + Tests are intentionally failing until the bug fix for #524 is applied. (#553) - 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, -- 2.52.0 From d7865f1b8a68d7060eaf7c035c8c090aee665add Mon Sep 17 00:00:00 2001 From: "Brent E. Edwards" Date: Wed, 4 Mar 2026 22:40:09 +0000 Subject: [PATCH 3/7] fix(test): address self-review findings on git-checkout bootstrap tests - Add @tdd @bug524 tags to both feature scenarios for selective execution - Move module-level CliRunner singleton to per-step instantiation for consistency with PR #566 pattern Refs: #553 --- features/resource_type_bootstrap_git.feature | 2 ++ features/steps/resource_type_bootstrap_git_steps.py | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/features/resource_type_bootstrap_git.feature b/features/resource_type_bootstrap_git.feature index 3b52913fc..26fa10521 100644 --- a/features/resource_type_bootstrap_git.feature +++ b/features/resource_type_bootstrap_git.feature @@ -8,6 +8,7 @@ Feature: Built-in git-checkout type bootstrap on initialization # ── Registry presence after bootstrap ────────────────────── + @tdd @bug524 Scenario: After initialization the git-checkout type exists in the resource type registry Given a freshly initialised resource registry service When bootstrap_builtin_types is called during initialisation @@ -18,6 +19,7 @@ Feature: Built-in git-checkout type bootstrap on initialization # ── CLI resource add succeeds ────────────────────────────── + @tdd @bug524 Scenario: agents resource add git-checkout succeeds without Resource type not found error Given a freshly initialised resource registry service And bootstrap_builtin_types is called during initialisation diff --git a/features/steps/resource_type_bootstrap_git_steps.py b/features/steps/resource_type_bootstrap_git_steps.py index fbf20015e..bec719019 100644 --- a/features/steps/resource_type_bootstrap_git_steps.py +++ b/features/steps/resource_type_bootstrap_git_steps.py @@ -20,9 +20,6 @@ from cleveragents.application.services.resource_registry_service import ( ) from cleveragents.infrastructure.database.models import Base -runner = CliRunner() - - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -81,7 +78,7 @@ def step_run_resource_add_cli(context: Context) -> None: "cleveragents.cli.commands.resource.get_container", return_value=mock_container, ): - result = runner.invoke( + result = CliRunner().invoke( resource_app, [ "add", -- 2.52.0 From 2bc1a6dd48c2856f9c12487e1a8b9a59469c06de Mon Sep 17 00:00:00 2001 From: "Brent E. Edwards" Date: Thu, 5 Mar 2026 20:35:55 +0000 Subject: [PATCH 4/7] fix(test): address hamza.khyari review #1986 findings on git-checkout bootstrap tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SPEC-1: Added genuine TDD failing Scenario 1 (@wip) that creates registry WITHOUT bootstrap and asserts git-checkout exists — reproduces bug #524. Existing scenarios retained as regression tests (no @wip since they pass). Added NOTE FOR FIX AUTHOR comment documenting fix-path expectations. - BUG-1: Removed colliding @when('I run "agents resource add..."') step. Replaced with uniquely-prefixed bootstrap-git step pattern that invokes resource_add() directly with mocked DI, avoiding AmbiguousStep collision with wildcard @when('I run "{command}"') in cli_plan_context_commands_steps. - BUG-2: Removed duplicate @then('the CLI exit code should be {code:d}'). Replaced with prefixed bootstrap-git assertion steps. - BUG-3: Removed duplicate @then('the CLI output should not contain..."). Replaced with prefixed bootstrap-git assertion steps. - TEST-1: Replaced bare MagicMock() with direct service patching via _PATCH_SERVICE, consistent with PR #567 pattern. - TEST-2: Updated Robot docs from 'expected to FAIL' to 'regression tests' since both Robot tests call bootstrap explicitly and pass. - CODE-1: Simplified hasattr guards on enum fields — removed redundant hasattr checks, using .value directly since ResourceKind and SandboxStrategy are always enums. - TEST-3: Added assertion on bootstrap_builtin_types() return value via new Then step 'the bootstrap-git registered types should include'. - Updated CHANGELOG from 'Two scenarios' to 'Three scenarios'. Refs: #553 --- CHANGELOG.md | 9 +- features/resource_type_bootstrap_git.feature | 40 ++- .../resource_type_bootstrap_git_steps.py | 250 +++++++++++------- robot/resource_type_bootstrap_git.robot | 7 +- 4 files changed, 185 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 512ed058c..374f3c215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ 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 TDD-style failing Behave BDD tests for the built-in `git-checkout` resource type - bootstrap. Two scenarios verify that `git-checkout` exists in the registry after init and - that `agents resource add git-checkout` succeeds. Includes Robot Framework smoke tests. - Tests are intentionally failing until the bug fix for #524 is applied. (#553) +- Added TDD-style Behave BDD tests for the built-in `git-checkout` resource type + bootstrap. Three scenarios: one failing TDD test reproducing bug #524 (no bootstrap + called during init), and two regression tests verifying `bootstrap_builtin_types()` + seeds correct data and `agents resource add git-checkout` succeeds. Includes Robot + Framework regression tests. (#553) - 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_git.feature b/features/resource_type_bootstrap_git.feature index 26fa10521..6f92ebc83 100644 --- a/features/resource_type_bootstrap_git.feature +++ b/features/resource_type_bootstrap_git.feature @@ -6,23 +6,39 @@ Feature: Built-in git-checkout type bootstrap on initialization I want the built-in git-checkout resource type to be available after initialization So that I can run "agents resource add git-checkout" without "Resource type not found" - # ── Registry presence after bootstrap ────────────────────── + # ── 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 git-checkout 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 @bug524 @wip + Scenario: git-checkout type is missing when bootstrap is not called during init + Given a bootstrap-git fresh in-memory resource registry without bootstrap + When I query the bootstrap-git resource type registry for "git-checkout" + Then the bootstrap-git resource type "git-checkout" should exist + + # ── Regression: bootstrap function itself works correctly ── @tdd @bug524 Scenario: After initialization the git-checkout type exists in the resource type registry - Given a freshly initialised resource registry service - When bootstrap_builtin_types is called during initialisation - Then the resource type "git-checkout" should be present in the registry - And the resource type "git-checkout" should have kind "physical" - And the resource type "git-checkout" should have sandbox_strategy "git_worktree" - And the resource type "git-checkout" should be user_addable + Given a bootstrap-git fresh in-memory resource registry with bootstrap + When I query the bootstrap-git resource type registry for "git-checkout" + Then the bootstrap-git resource type "git-checkout" should exist + And the bootstrap-git resource type "git-checkout" should have kind "physical" + And the bootstrap-git resource type "git-checkout" should have sandbox_strategy "git_worktree" + And the bootstrap-git resource type "git-checkout" should be user_addable + And the bootstrap-git registered types should include "git-checkout" # ── CLI resource add succeeds ────────────────────────────── @tdd @bug524 Scenario: agents resource add git-checkout succeeds without Resource type not found error - Given a freshly initialised resource registry service - And bootstrap_builtin_types is called during initialisation - When I run "agents resource add git-checkout local/test --path /tmp/repo --branch main" - Then the CLI exit code should be 0 - And the CLI output should not contain "Resource type not found" + Given a bootstrap-git fresh in-memory resource registry with bootstrap + When I run bootstrap-git resource add for type "git-checkout" named "local/test" with path "/tmp/repo" and branch "main" + Then the bootstrap-git resource add command should succeed + And the bootstrap-git resource add output should not contain "Resource type not found" diff --git a/features/steps/resource_type_bootstrap_git_steps.py b/features/steps/resource_type_bootstrap_git_steps.py index bec719019..c12c6f488 100644 --- a/features/steps/resource_type_bootstrap_git_steps.py +++ b/features/steps/resource_type_bootstrap_git_steps.py @@ -1,164 +1,210 @@ """Step definitions for resource_type_bootstrap_git.feature. -These tests target bug #524 and are expected to fail until the fix is applied. -bootstrap_builtin_types() is never called during initialization, so the -git-checkout built-in resource type is missing from the registry at runtime. +These tests target bug #524. The first scenario (without bootstrap) is a +genuine TDD failing test: it creates a fresh registry *without* calling +``bootstrap_builtin_types()`` and asserts ``git-checkout`` 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 unittest.mock import MagicMock, patch +from io import StringIO +from typing import Any +from unittest.mock import patch from behave import given, then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] +from rich.console import Console from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from typer.testing import CliRunner from cleveragents.application.services.resource_registry_service import ( ResourceRegistryService, ) +from cleveragents.core.exceptions import NotFoundError from cleveragents.infrastructure.database.models import Base -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- +# ── Helpers ────────────────────────────────────────────────── + +_PATCH_SERVICE = "cleveragents.cli.commands.resource._get_registry_service" +_PATCH_CONSOLE = "cleveragents.cli.commands.resource.console" -def _fresh_service() -> ResourceRegistryService: - """Create a ResourceRegistryService backed by an in-memory SQLite DB.""" +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) - return ResourceRegistryService(session_factory=factory) + service = ResourceRegistryService(session_factory=factory) + if run_bootstrap: + context.bootstrap_git_registered = service.bootstrap_builtin_types() # type: ignore[attr-defined] + context.bootstrap_git_service = service # type: ignore[attr-defined] + return service -# --------------------------------------------------------------------------- -# Given steps -# --------------------------------------------------------------------------- +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: + failed = True + failure_reason = f"SystemExit: {exc.code}" + except Exception as exc: + failed = True + failure_reason = f"{type(exc).__name__}: {exc}" + + output = buf.getvalue() + if failure_reason: + output = output + "\n" + failure_reason + return output, failed -@given("a freshly initialised resource registry service") -def step_fresh_registry(context: Context) -> None: - """Create a fresh in-memory registry service (no bootstrap yet).""" - context.bootstrap_service = _fresh_service() # type: ignore[attr-defined] +# ── Given steps ────────────────────────────────────────────── -# --------------------------------------------------------------------------- -# When steps -# --------------------------------------------------------------------------- +@given("a bootstrap-git 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_git_output = "" # type: ignore[attr-defined] + context.bootstrap_git_failed = False # type: ignore[attr-defined] -@when("bootstrap_builtin_types is called during initialisation") -def step_call_bootstrap(context: Context) -> None: - """Call bootstrap_builtin_types, which should seed built-in types.""" - service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] - context.bootstrap_registered = service.bootstrap_builtin_types() # type: ignore[attr-defined] +@given("a bootstrap-git 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 #524: the registry is empty because + ``bootstrap_builtin_types()`` is never called during initialization. + """ + _make_service(context, run_bootstrap=False) + context.bootstrap_git_output = "" # type: ignore[attr-defined] + context.bootstrap_git_failed = False # type: ignore[attr-defined] -@given("bootstrap_builtin_types is called during initialisation") -def step_given_bootstrap(context: Context) -> None: - """Call bootstrap as a precondition (Given variant).""" - step_call_bootstrap(context) +# ── When steps ─────────────────────────────────────────────── + + +@when('I query the bootstrap-git 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_git_service # type: ignore[attr-defined] + try: + context.bootstrap_git_type_spec = service.show_type(name) # type: ignore[attr-defined] + context.bootstrap_git_type_found = True # type: ignore[attr-defined] + except NotFoundError: + context.bootstrap_git_type_spec = None # type: ignore[attr-defined] + context.bootstrap_git_type_found = False # type: ignore[attr-defined] @when( - 'I run "agents resource add git-checkout local/test --path /tmp/repo --branch main"' + 'I run bootstrap-git resource add for type "{type_name}" named "{name}" with path "{path}" and branch "{branch}"' ) -def step_run_resource_add_cli(context: Context) -> None: - """Invoke the resource add CLI command via CliRunner with mocked DI.""" - from cleveragents.cli.commands.resource import app as resource_app - - service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] - - mock_container = MagicMock() - mock_container.resource_registry_service.return_value = service +def step_run_resource_add_git( + context: Context, type_name: str, name: str, path: str, branch: str +) -> None: + """Run resource add command via the CLI function.""" + from cleveragents.cli.commands.resource import resource_add with patch( - "cleveragents.cli.commands.resource.get_container", - return_value=mock_container, + _PATCH_SERVICE, + return_value=context.bootstrap_git_service, # type: ignore[attr-defined] ): - result = CliRunner().invoke( - resource_app, - [ - "add", - "git-checkout", - "local/test", - "--path", - "/tmp/repo", - "--branch", - "main", - ], + output, failed = _capture_output( + resource_add, + type_name=type_name, + name=name, + path=path, + branch=branch, + description=None, + image=None, + read_only=False, + fmt="rich", ) - - context.cli_result = result # type: ignore[attr-defined] + context.bootstrap_git_output = output # type: ignore[attr-defined] + context.bootstrap_git_failed = failed # type: ignore[attr-defined] -# --------------------------------------------------------------------------- -# Then steps -# --------------------------------------------------------------------------- +# ── Then steps ─────────────────────────────────────────────── -@then('the resource type "{name}" should be present in the registry') -def step_type_present(context: Context, name: str) -> None: - """Assert the named resource type exists in the registry.""" - service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] - all_types = service.list_types() - type_names = [t.name for t in all_types] - assert name in type_names, f"Expected '{name}' in registry, got: {type_names}" - - -@then('the resource type "{name}" should have kind "{kind}"') -def step_type_kind(context: Context, name: str, kind: str) -> None: - """Assert the resource type has the expected resource_kind.""" - service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] - spec = service.show_type(name) - actual = ( - spec.resource_kind.value - if hasattr(spec.resource_kind, "value") - else str(spec.resource_kind) +@then('the bootstrap-git 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_git_type_found is True, ( # type: ignore[attr-defined] + f"Expected resource type '{name}' to exist in the registry after " + f"bootstrap, but it was not found. This is bug #524: " + f"bootstrap_builtin_types() is never called during initialization." ) + + +@then('the bootstrap-git resource type "{name}" should have kind "{kind}"') +def step_type_kind(context: Context, name: str, kind: str) -> None: + """Assert the resource type has the expected kind.""" + spec = context.bootstrap_git_type_spec # type: ignore[attr-defined] + actual = spec.resource_kind.value assert actual == kind, f"Expected kind '{kind}', got '{actual}'" -@then('the resource type "{name}" should have sandbox_strategy "{strategy}"') +@then( + 'the bootstrap-git resource type "{name}" should have sandbox_strategy "{strategy}"' +) def step_type_sandbox(context: Context, name: str, strategy: str) -> None: - """Assert the resource type has the expected sandbox_strategy.""" - service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] - spec = service.show_type(name) - actual = ( - spec.sandbox_strategy.value - if hasattr(spec.sandbox_strategy, "value") - else str(spec.sandbox_strategy) - ) - assert actual == strategy, f"Expected strategy '{strategy}', got '{actual}'" + """Assert the resource type has the expected sandbox strategy.""" + spec = context.bootstrap_git_type_spec # type: ignore[attr-defined] + actual = spec.sandbox_strategy.value + assert actual == strategy, f"Expected sandbox_strategy '{strategy}', got '{actual}'" -@then('the resource type "{name}" should be user_addable') +@then('the bootstrap-git resource type "{name}" should be user_addable') def step_type_user_addable(context: Context, name: str) -> None: """Assert the resource type is user-addable.""" - service: ResourceRegistryService = context.bootstrap_service # type: ignore[attr-defined] - spec = service.show_type(name) + spec = context.bootstrap_git_type_spec # type: ignore[attr-defined] assert spec.user_addable is True, ( f"Expected user_addable=True, got {spec.user_addable}" ) -@then("the CLI exit code should be {code:d}") -def step_cli_exit_code(context: Context, code: int) -> None: - """Assert the CLI runner result exit code.""" - result = context.cli_result # type: ignore[attr-defined] - assert result.exit_code == code, ( - f"Expected exit code {code}, got {result.exit_code}.\n" - f"Output: {result.output}\n" - f"Exception: {result.exception!r}" +@then('the bootstrap-git registered types should include "{name}"') +def step_registered_includes(context: Context, name: str) -> None: + """Assert the bootstrap return value includes the named type.""" + registered = context.bootstrap_git_registered # type: ignore[attr-defined] + assert name in registered, ( + f"Expected '{name}' in bootstrap_builtin_types() return value, " + f"got: {registered}" ) -@then('the CLI output should not contain "{text}"') -def step_cli_no_text(context: Context, text: str) -> None: - """Assert the CLI output does not contain the given text.""" - result = context.cli_result # type: ignore[attr-defined] - assert text not in result.output, ( - f"Did not expect '{text}' in CLI output, but found it:\n{result.output}" +@then("the bootstrap-git 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_git_failed is not True, ( # type: ignore[attr-defined] + f"Expected 'resource add' to succeed, but it failed. " + f"Output: {context.bootstrap_git_output!r}. " + f"This is bug #524: bootstrap_builtin_types() is never called." + ) + + +@then('the bootstrap-git 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_git_output, ( # type: ignore[attr-defined] + f"Output unexpectedly contained '{text}'. " + f"Full output: {context.bootstrap_git_output!r}" ) diff --git a/robot/resource_type_bootstrap_git.robot b/robot/resource_type_bootstrap_git.robot index 67d7f3db6..15d5e1b95 100644 --- a/robot/resource_type_bootstrap_git.robot +++ b/robot/resource_type_bootstrap_git.robot @@ -1,7 +1,8 @@ *** Settings *** -Documentation Integration smoke tests for built-in git-checkout type bootstrap (bug #524). -... These tests are expected to FAIL until the fix is applied because -... bootstrap_builtin_types() is never called during initialization. +Documentation Regression tests for built-in git-checkout type bootstrap (bug #524). +... These tests verify that bootstrap_builtin_types() correctly seeds +... git-checkout into the registry and that resource registration succeeds +... after bootstrap. Both tests call bootstrap explicitly and should pass. Library Process Library OperatingSystem Resource common.resource -- 2.52.0 From 783b748af9a46255ea8a2c8542db036d8dfcf991 Mon Sep 17 00:00:00 2001 From: Brent Edwards Date: Thu, 5 Mar 2026 22:02:05 +0000 Subject: [PATCH 5/7] fix(test): add tags = ~@wip to behave.ini to exclude @wip scenarios Addresses BUG-4 from hamza.khyari Round 2 review: @wip scenarios broke `nox -s unit_tests` because behave.ini had no wip exclusion. --- behave.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/behave.ini b/behave.ini index b19e710cd..25881a92c 100644 --- a/behave.ini +++ b/behave.ini @@ -1,4 +1,5 @@ [behave] paths = features +tags = ~@wip stdout_capture = no stderr_capture = no -- 2.52.0 From a6d1fb37ceb0c1b14df72a25b9f24f04f968a3a0 Mon Sep 17 00:00:00 2001 From: Brent Edwards Date: Thu, 5 Mar 2026 22:02:32 +0000 Subject: [PATCH 6/7] fix(test): fix _capture_output to treat SystemExit(0) as success (TEST-4) SystemExit(0) and SystemExit(None) are normal termination, not failures. Only set failed=True when exit code is non-zero. --- features/steps/resource_type_bootstrap_git_steps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/features/steps/resource_type_bootstrap_git_steps.py b/features/steps/resource_type_bootstrap_git_steps.py index c12c6f488..b57ee7919 100644 --- a/features/steps/resource_type_bootstrap_git_steps.py +++ b/features/steps/resource_type_bootstrap_git_steps.py @@ -63,8 +63,9 @@ def _capture_output(func: Any, *args: Any, **kwargs: Any) -> tuple[str, bool]: try: func(*args, **kwargs) except SystemExit as exc: - failed = True - failure_reason = f"SystemExit: {exc.code}" + if exc.code not in (None, 0): + failed = True + failure_reason = f"SystemExit: {exc.code}" except Exception as exc: failed = True failure_reason = f"{type(exc).__name__}: {exc}" -- 2.52.0 From dcd55967f5583d7569acf33aee2693d1fc4ba802 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sat, 7 Mar 2026 06:14:25 +0000 Subject: [PATCH 7/7] fix(resource): call bootstrap_builtin_types during initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a call to bootstrap_builtin_types() in init_command() (project.py) immediately after initialize_project() returns. This seeds the built-in resource types (fs-directory, git-checkout, etc.) into the database so that "resource add" commands succeed without "Resource type not found" errors. The call is idempotent — invoking it multiple times will not create duplicate types. Also fix the TDD robot test (resource_type_bootstrap_git.robot) to initialize a project before running "resource add", and fix a pre-existing parallel test failure in plan_commands_new_coverage where unittest.mock.patch could not reliably intercept PlanApplyService under behave-parallel fork() workers. ISSUES CLOSED: #523, #524 --- .../steps/plan_commands_new_coverage_steps.py | 44 ++++++++++++++----- robot/resource_type_bootstrap_git.robot | 15 +++++-- src/cleveragents/cli/commands/project.py | 12 +++++ 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/features/steps/plan_commands_new_coverage_steps.py b/features/steps/plan_commands_new_coverage_steps.py index 6688ff8cf..935f8ee66 100644 --- a/features/steps/plan_commands_new_coverage_steps.py +++ b/features/steps/plan_commands_new_coverage_steps.py @@ -170,23 +170,40 @@ use_step_matcher("parse") @when("new_cov I call _get_apply_service directly") def step_new_cov_call_get_apply(context: Context) -> None: - """Call the real _get_apply_service, mocking only its dependencies. + """Call the real ``_get_apply_service``, mocking its dependencies. This exercises the actual function body (import, lifecycle lookup, - PlanApplyService construction) to cover lines 1817, 1821, 1822. + ``PlanApplyService`` construction) to cover lines 1817, 1821, 1822. + + Both ``_get_lifecycle_service`` **and** ``PlanApplyService`` are + patched. The class is patched at two locations — the canonical + source module and the plan module (with ``create=True``) — so the + lazy ``from … import PlanApplyService`` inside the function always + resolves to our mock, even under ``behave-parallel``'s + ``fork()``-based workers. """ from cleveragents.cli.commands.plan import _get_apply_service + mock_lifecycle = MagicMock() + mock_pas_cls = MagicMock() mock_pas_instance = MagicMock() - mock_pas_class = MagicMock(return_value=mock_pas_instance) + mock_pas_cls.return_value = mock_pas_instance with ( - patch(_PATCH_LIFECYCLE, return_value=context.new_cov_mock_lifecycle), - patch(_PATCH_PAS_CLASS, mock_pas_class), + patch(_PATCH_LIFECYCLE, return_value=mock_lifecycle), + patch( + "cleveragents.cli.commands.plan.PlanApplyService", + mock_pas_cls, + create=True, + ), + patch(_PATCH_PAS_CLASS, mock_pas_cls), ): - context.new_cov_apply_result = _get_apply_service() - context.new_cov_pas_class = mock_pas_class - context.new_cov_pas_instance = mock_pas_instance + result = _get_apply_service() + + context.new_cov_apply_result = result + context.new_cov_mock_lifecycle_used = mock_lifecycle + context.new_cov_mock_pas_cls = mock_pas_cls + context.new_cov_mock_pas_instance = mock_pas_instance # --------------------------------------------------------------------------- @@ -218,13 +235,16 @@ def step_new_cov_output_contains(context: Context, text: str) -> None: @then("new_cov the returned object should be a PlanApplyService instance") def step_new_cov_result_is_pas(context: Context) -> None: - assert context.new_cov_apply_result is context.new_cov_pas_instance, ( - f"Expected the mock PAS instance, got {type(context.new_cov_apply_result).__name__}" + result = context.new_cov_apply_result + expected = context.new_cov_mock_pas_instance + assert result is expected, ( + f"Expected _get_apply_service to return the mock PlanApplyService " + f"instance, got {type(result).__name__}" ) @then("new_cov PlanApplyService was constructed with the lifecycle service") def step_new_cov_pas_called_with_lifecycle(context: Context) -> None: - context.new_cov_pas_class.assert_called_once_with( - lifecycle_service=context.new_cov_mock_lifecycle, + context.new_cov_mock_pas_cls.assert_called_once_with( + lifecycle_service=context.new_cov_mock_lifecycle_used, ) diff --git a/robot/resource_type_bootstrap_git.robot b/robot/resource_type_bootstrap_git.robot index 15d5e1b95..493c1ad14 100644 --- a/robot/resource_type_bootstrap_git.robot +++ b/robot/resource_type_bootstrap_git.robot @@ -12,16 +12,23 @@ Suite Teardown Cleanup Test Environment *** Test Cases *** Resource Add Git Checkout Should Not Fail With Type Not Found - [Documentation] Run "agents resource add git-checkout" and verify it does not - ... produce a "Resource type not found" error. This currently fails - ... because bootstrap_builtin_types() is never called. + [Documentation] Initialize a project then run "agents resource add git-checkout" + ... and verify it does not produce a "Resource type not found" error. + ... bootstrap_builtin_types() is called during project init. + ${proj_dir}= Set Variable ${TEMPDIR}${/}git_checkout_bootstrap_test + Create Directory ${proj_dir} + ${init_result}= Run Process ${PYTHON} -m cleveragents init test-git-bootstrap + ... cwd=${proj_dir} timeout=120s + Should Be Equal As Integers ${init_result.rc} 0 + ... msg=Project init failed: rc=${init_result.rc} stderr=${init_result.stderr} ${result}= Run Process ${PYTHON} -m cleveragents resource add ... git-checkout local/test --path /tmp/repo --branch main - ... timeout=60s + ... cwd=${proj_dir} timeout=60s Should Not Contain ${result.stdout} Resource type not found Should Not Contain ${result.stderr} Resource type not found Should Be Equal As Integers ${result.rc} 0 ... msg=Expected exit code 0 but got ${result.rc}. stdout: ${result.stdout} stderr: ${result.stderr} + [Teardown] Run Keyword And Ignore Error Remove Directory ${proj_dir} recursive=True Git Checkout Type Exists After Bootstrap [Documentation] Directly call bootstrap_builtin_types() and verify git-checkout diff --git a/src/cleveragents/cli/commands/project.py b/src/cleveragents/cli/commands/project.py index e7503f13e..1d45a246a 100644 --- a/src/cleveragents/cli/commands/project.py +++ b/src/cleveragents/cli/commands/project.py @@ -200,6 +200,18 @@ def init_command( apply_default_filters=default_filters, ) + # Bootstrap built-in resource types (fs-directory, git-checkout, etc.) + # into the database after schema creation. This must happen after + # initialize_project() because the database tables are created there + # via unit_of_work.init_database(). The call is idempotent — calling + # it multiple times will not create duplicate types. + from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, + ) + + resource_registry: ResourceRegistryService = container.resource_registry_service() + resource_registry.bootstrap_builtin_types() + console.print( Panel( f"[green]✓[/green] Project '{project.name}' " -- 2.52.0