Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dcd55967f5 | |||
| a6d1fb37ce | |||
| 783b748af9 | |||
| 2bc1a6dd48 | |||
| d7865f1b8a | |||
| dbf324352a | |||
| 23c1678058 |
@@ -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,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}"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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}' "
|
||||
|
||||
Reference in New Issue
Block a user