fix(resource): call bootstrap_builtin_types during initialization #626

Merged
freemo merged 2 commits from feature/m3-fix-resource-bootstrap-rebased into master 2026-03-07 19:41:33 +00:00
13 changed files with 82 additions and 39 deletions
+5
View File
@@ -38,6 +38,11 @@
operations, triple extraction, error handling, cross-analyzer URI scheme and
confidence checks), 6 Robot Framework integration smoke tests, and updated
`__init__.py` exports. (#588)
- 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,
-6
View File
@@ -1,10 +1,4 @@
[behave]
paths = features
# Exclude @wip scenarios globally so TDD failing tests do not break CI.
# Any contributor tagging a scenario @wip will have it skipped by default.
# NOTE: --tags=@wip on the CLI will NOT work; Behave ANDs ini and CLI tags.
# To run a @wip scenario locally, target it by file/line number:
# behave features/<file>.feature:<line>
tags = ~@wip
stdout_capture = no
stderr_capture = no
+4 -4
View File
@@ -13,14 +13,14 @@ Feature: CLI init --yes flag for non-interactive initialization
I want to run "agents init --yes" for non-interactive initialization
So that I can skip interactive prompts and use sensible defaults
@tdd @bug522 @wip
@tdd @bug522
Scenario: agents init --yes completes without error
Given I have a temporary project directory for init
When I run agents init with the --yes flag
Then the init command should exit with code 0
And the project service initialize_project should have been called
@tdd @bug522 @wip
@tdd @bug522
Scenario: --yes suppresses interactive prompts
Given I have a temporary project directory for init
When I run agents init with the --yes flag
@@ -28,7 +28,7 @@ Feature: CLI init --yes flag for non-interactive initialization
And the init output should contain "Initialized (non-interactive)"
And no interactive prompt should have been presented
@tdd @bug522 @wip
@tdd @bug522
Scenario: -y short-form alias completes without error
Given I have a temporary project directory for init
When I run agents init with the -y flag
@@ -36,7 +36,7 @@ Feature: CLI init --yes flag for non-interactive initialization
And the init output should contain "Initialized (non-interactive)"
And the project service initialize_project should have been called
@tdd @bug522 @wip
@tdd @bug522
Scenario: Output includes expected initialization summary
Given I have a temporary project directory for init
When I run agents init with the --yes flag
+1 -6
View File
@@ -8,12 +8,7 @@ Feature: Resource CLI commands
# ---- Resource Type List ----
Scenario: List resource types when empty
When I run resource type list
Then the resource output should contain "No resource types registered"
Scenario: List resource types after bootstrap
Given built-in types are bootstrapped
Scenario: List resource types shows built-in types after init
When I run resource type list
Then the resource output should contain "git-checkout"
And the resource output should contain "fs-directory"
+1 -1
View File
@@ -17,7 +17,7 @@ Feature: Built-in fs-directory Resource Type Bootstrap
# ResourceRegistryService.__init__(), you will need to update the Given
# step to exercise the init path instead of constructing a bare service.
@tdd @bug523 @wip
@tdd @bug523
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"
+1 -1
View File
@@ -16,7 +16,7 @@ Feature: Built-in git-checkout type bootstrap on initialization
# ResourceRegistryService.__init__(), you will need to update the Given
# step to exercise the init path instead of constructing a bare service.
@tdd @bug524 @wip
@tdd @bug524
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"
@@ -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,
)
@@ -111,7 +111,14 @@ def step_init_resource_svc_db(context: Any) -> None:
def step_bootstrap_builtins(context: Any) -> None:
result = context.resource_service.bootstrap_builtin_types()
context.last_bootstrap_count = len(result)
context.bootstrap_names = result
# Types may have been auto-bootstrapped in __init__; fall back to
# querying the database so assertion steps see all registered types.
if result:
context.bootstrap_names = result
else:
context.bootstrap_names = [
t.name for t in context.resource_service.list_types()
]
@then('the bootstrap should register "{name}"')
@@ -37,15 +37,20 @@ _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).
Built-in types are now auto-bootstrapped in ``__init__``. When
*run_bootstrap* is ``True`` the step records the list of registered
type names for assertion steps; when ``False`` the service is still
created (and auto-bootstrapped) but no list is recorded.
"""
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:
context.bootstrap_git_registered = service.bootstrap_builtin_types() # type: ignore[attr-defined]
# Types are auto-bootstrapped in __init__; record them for assertions.
context.bootstrap_git_registered = [ # type: ignore[attr-defined]
t.name for t in service.list_types()
]
context.bootstrap_git_service = service # type: ignore[attr-defined]
return service
+3 -3
View File
@@ -26,9 +26,9 @@ Resource Registry Bootstrap Creates Built-in Types
... Base.metadata.create_all(engine)
... sf = sessionmaker(bind=engine)
... svc = ResourceRegistryService(session_factory=sf)
... result = svc.bootstrap_builtin_types()
... assert "git-checkout" in result
... assert "fs-directory" in result
... types = [t.name for t in svc.list_types()]
... assert "git-checkout" in types, f"git-checkout not in {types}"
... assert "fs-directory" in types, f"fs-directory not in {types}"
... print("PASS")
${result}= Run Process ${PYTHON} -c ${script} env:PYTHONPATH=src
Should Be Equal As Integers ${result.rc} 0
+1 -1
View File
@@ -23,7 +23,7 @@ Bootstrap Seeds Fs Directory Type Into Registry
... Base.metadata.create_all(engine)
... factory = sessionmaker(bind=engine, expire_on_commit=False)
... service = ResourceRegistryService(session_factory=factory)
... registered = service.bootstrap_builtin_types()
... registered = [t.name for t in service.list_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}"
+1 -1
View File
@@ -51,7 +51,7 @@ Git Checkout Type Exists After Bootstrap
... Base.metadata.create_all(engine)
... factory = sessionmaker(bind=engine, expire_on_commit=False)
... service = ResourceRegistryService(session_factory=factory)
... registered = service.bootstrap_builtin_types()
... registered = [t.name for t in service.list_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}"
@@ -229,10 +229,27 @@ class ResourceRegistryService:
) -> None:
"""Initialize the resource registry service.
Automatically seeds built-in resource types (``fs-directory``,
``git-checkout``, etc.) on construction so they are available
immediately after ``agents init``. The bootstrap is idempotent
and gracefully skipped when the database tables do not yet exist
(e.g. before migrations have run).
Args:
session_factory: Callable returning a SQLAlchemy Session.
"""
self._session_factory = session_factory
try:
self.bootstrap_builtin_types()
except Exception:
# Tables may not exist yet (pre-migration) or the session may
# be a test mock. Callers that need built-in types before
# migrations complete should invoke bootstrap_builtin_types()
# explicitly after the schema is ready.
logger.debug(
"Auto-bootstrap of built-in resource types deferred "
"(tables may not exist yet)"
)
def _session(self) -> Any:
"""Convenience helper to obtain a session."""