Compare commits

...

7 Commits

Author SHA1 Message Date
freemo dcd55967f5 fix(resource): call bootstrap_builtin_types during initialization
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 17s
CI / quality (pull_request) Successful in 19s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 2m31s
CI / integration_tests (pull_request) Successful in 3m4s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 5m11s
CI / benchmark-regression (pull_request) Successful in 28m35s
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
2026-03-07 06:14:25 +00:00
brent.edwards a6d1fb37ce 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.
2026-03-07 01:50:27 +00:00
brent.edwards 783b748af9 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.
2026-03-07 01:50:27 +00:00
brent.edwards 2bc1a6dd48 fix(test): address hamza.khyari review #1986 findings on git-checkout bootstrap tests
- 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
2026-03-07 01:50:27 +00:00
brent.edwards d7865f1b8a 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
2026-03-07 01:49:50 +00:00
brent.edwards dbf324352a docs(changelog): add entry for git-checkout bootstrap test scaffolding
ISSUES CLOSED: #553
2026-03-07 01:49:50 +00:00
brent.edwards 23c1678058 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
2026-03-07 01:49:11 +00:00
7 changed files with 363 additions and 12 deletions
+5
View File
@@ -7,6 +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 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
View File
@@ -1,4 +1,5 @@
[behave]
paths = features
tags = ~@wip
stdout_capture = no
stderr_capture = no
@@ -0,0 +1,44 @@
# 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"
# 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 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 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"
@@ -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,
)
@@ -0,0 +1,211 @@
"""Step definitions for resource_type_bootstrap_git.feature.
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 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 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:
context.bootstrap_git_registered = service.bootstrap_builtin_types() # type: ignore[attr-defined]
context.bootstrap_git_service = service # type: ignore[attr-defined]
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:
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}"
output = buf.getvalue()
if failure_reason:
output = output + "\n" + failure_reason
return output, failed
# ── Given 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]
@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]
# ── 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 bootstrap-git resource add for type "{type_name}" named "{name}" with path "{path}" and branch "{branch}"'
)
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(
_PATCH_SERVICE,
return_value=context.bootstrap_git_service, # type: ignore[attr-defined]
):
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.bootstrap_git_output = output # type: ignore[attr-defined]
context.bootstrap_git_failed = failed # type: ignore[attr-defined]
# ── Then steps ───────────────────────────────────────────────
@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 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."""
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 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."""
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 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 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}"
)
+58
View File
@@ -0,0 +1,58 @@
*** Settings ***
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
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Test Cases ***
Resource Add Git Checkout Should Not Fail With Type Not Found
[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
... 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
... 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
+12
View File
@@ -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}' "