fix(resource): call bootstrap_builtin_types during initialization #626
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user