Files
cleveragents-core/features/steps/resource_handler_crud_steps.py
freemo 02250473ad fix(ci): restore all CI quality gates to passing on master
Fix all failing CI quality gates (lint, unit_tests, format) without
suppressing any quality enforcement.

Root causes and fixes:

1. Format: features/steps/plan_namespaced_name_tdd_steps.py had trailing
   whitespace; fixed by running ruff format.

2. Unit tests - A2A JSON-RPC 2.0 migration (commit 9c6d6915) renamed
   A2aRequest fields (operation→method, request_id→id, a2a_version→jsonrpc)
   and A2aResponse fields (status+data→result, request_id→id) but did not
   update all step files and feature files:
   - a2a_jsonrpc_wire_format_steps.py: added use_step_matcher('re') and
     reset to 'parse' at end to prevent parallel test interference
   - a2a_facade_wiring_steps.py: updated operation= to method=, .status/.data
     to .result
   - a2a_facade_steps.py: updated request_id→id, a2a_version→jsonrpc,
     A2aResponse(request_id=..., status=...) to new API
   - m6_facade_steps.py: updated all old API usage
   - devcontainer_cleanup_steps.py: updated A2aRequest(operation=...)
   - plan_prompt_command_steps.py: updated A2aRequest(operation=...)
   - wf03_plan_prompt_confidence_steps.py: updated A2aRequest(operation=...)
   - consolidated_misc.feature: updated old A2aRequest/A2aResponse scenarios

3. Unit tests - Session CLI output changed (commit 0d5d9cf0 and others):
   - 'Session Created' → 'Session created' (lowercase)
   - 'Session Details' → 'Session Summary'
   - 'Sessions (N total)' → 'Sessions'
   - session list JSON: top-level 'total' → nested 'summary.total'
   - Fixed in: session_cli.feature, session_cli_coverage_boost.feature,
     session_cli_uncovered_branches.feature, session_list_error.feature,
     tdd_session_create_persist_steps.py

4. Unit tests - Plan list output changed (commit 1a07a891):
   - 'V3 Lifecycle Plans' → 'Plans'
   - 'Lifecycle Plans' → 'Plans'
   - Name column removed (restored in source)
   - Invariants column removed (restored in source)
   - Project truncation removed (restored in source)
   - Fixed in: plan_cli_cancel_revert_coverage.feature,
     plan_lifecycle_cli_coverage.feature, plan_cli_coverage_boost_steps.py,
     plan.py (source code restored)

5. Unit tests - Plan apply command now requires ULID (commit 300a5d6d):
   - plan_cli_coverage_r3.feature: updated 'PLAN-001' to valid ULID
   - plan_cli_coverage_r3_steps.py: added --yes flag, added new step for
     no-eligible-plans path

6. Unit tests - Various source code bugs:
   - ThoughtBlock: converted from @dataclass to Pydantic BaseModel
     (architecture test requires all dataclasses to use Pydantic)
   - session.py: added DatabaseError handling to export, import, tell commands
   - database.py: fixed rollback_to() to reuse checkpoint connection for writes
   - database.py: added _get_checkpoint_conn() helper
   - check-tls-cert.py: fixed SSLCertVerificationError.reason AttributeError

7. Unit tests - Test step bugs:
   - error_recovery_coverage_boost_steps.py: fixed invalid ULID _PLAN_ID
   - session_service_coverage_steps.py: fixed 'sha256:' prefix bug in checksum
   - database_models_new_coverage_steps.py: added 'name' field to session mock
   - async_audit_recording_steps.py: fixed Settings(audit_async=False) via env var
   - coverage_threshold_config_steps.py: added --coverage-min pattern support
   - m5_acms_smoke_steps.py: updated usage hint text
   - actor_cli_yaml_steps.py: updated 'Removed actor' → 'Actor removed'
   - aimodelscredentials_steps.py: set context.imported_class in import step
   - domain_base_model.feature: added missing 'When I examine model_config' step
   - tui_first_run_steps.py: fixed module reload to restore cleveragents.tui.*
     modules after test (prevented patch interference in subsequent tests)
   - tui_first_run_steps.py: added set_search('') step for empty string
   - resource_handler_base_coverage_r3_steps.py: use _MinimalHandler instead
     of DatabaseResourceHandler for NotImplementedError tests
   - resource_handler_crud.feature: updated to test new DatabaseHandler behavior
   - resource_handler_sandbox.feature: updated to test new DatabaseHandler behavior
   - tdd_json_decode_crash_persistence.feature: fixed @tdd_bug → @tdd_issue tags

8. Parallel test interference:
   - All step files using use_step_matcher('re') now reset to 'parse' at end
     to prevent global matcher state leaking to subsequent step files
2026-04-04 20:38:16 +00:00

476 lines
16 KiB
Python

"""Step definitions for resource_handler_crud.feature.
Tests CRUD and discovery operations (read, write, delete, list_children,
diff, discover_children) for GitCheckoutHandler and FsDirectoryHandler.
Issue #827: ResourceHandler CRUD and discovery methods.
"""
from __future__ import annotations
import subprocess
import tempfile
from pathlib import Path
from behave import given, then, when
from behave.runner import Context
from cleveragents.domain.models.core.resource import (
PhysVirt,
Resource,
ResourceCapabilities,
)
from cleveragents.resource.handlers.database import DatabaseResourceHandler
from cleveragents.resource.handlers.fs_directory import FsDirectoryHandler
from cleveragents.resource.handlers.git_checkout import GitCheckoutHandler
__all__: list[str] = []
_ULID_COUNTER = 0
def _next_ulid() -> str:
"""Generate a valid 26-char Crockford Base32 ID for tests."""
global _ULID_COUNTER
_ULID_COUNTER += 1
cb32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
n = _ULID_COUNTER
chars: list[str] = []
for _ in range(26):
chars.append(cb32[n % 32])
n //= 32
return "".join(reversed(chars))
def _make_resource(rtype: str, location: str, rid: str | None = None) -> Resource:
return Resource(
resource_id=rid or _next_ulid(),
resource_type_name=rtype,
classification=PhysVirt.PHYSICAL,
location=location,
capabilities=ResourceCapabilities(
readable=True, writable=True, sandboxable=True
),
)
# ---------------------------------------------------------------------------
# Given: temp directory setup
# ---------------------------------------------------------------------------
@given('a temp directory with file "{fname}" containing "{content}"')
def step_given_temp_dir_with_file(context: Context, fname: str, content: str) -> None:
context.crud_tmpdir = tempfile.mkdtemp(prefix="crud_fs_")
Path(context.crud_tmpdir, fname).write_text(content, encoding="utf-8")
@given('a temp sub-file "{fname}" containing "{content}"')
def step_given_temp_sub_file(context: Context, fname: str, content: str) -> None:
Path(context.crud_tmpdir, fname).write_text(content, encoding="utf-8")
@given('a temp sub-directory "{dirname}"')
def step_given_temp_sub_dir(context: Context, dirname: str) -> None:
Path(context.crud_tmpdir, dirname).mkdir(parents=True, exist_ok=True)
@given("an empty temp directory")
def step_given_empty_temp_dir(context: Context) -> None:
context.crud_tmpdir = tempfile.mkdtemp(prefix="crud_fs_")
@given("an fs-directory resource pointing to that directory")
def step_given_fs_resource(context: Context) -> None:
context.crud_handler = FsDirectoryHandler()
context.crud_resource = _make_resource("fs-directory", context.crud_tmpdir)
# -- Second temp directory for diff scenarios
@given('a second temp directory with file "{fname}" containing "{content}"')
def step_given_second_temp_dir(context: Context, fname: str, content: str) -> None:
context.crud_tmpdir2 = tempfile.mkdtemp(prefix="crud_fs2_")
Path(context.crud_tmpdir2, fname).write_text(content, encoding="utf-8")
@given('a second temp sub-file "{fname}" containing "{content}"')
def step_given_second_temp_sub_file(context: Context, fname: str, content: str) -> None:
Path(context.crud_tmpdir2, fname).write_text(content, encoding="utf-8")
# ---------------------------------------------------------------------------
# Given: temp git repo setup
# ---------------------------------------------------------------------------
@given('a temp git repo with file "{fname}" containing "{content}"')
def step_given_temp_git_repo(context: Context, fname: str, content: str) -> None:
context.crud_tmpdir = tempfile.mkdtemp(prefix="crud_git_")
subprocess.run(
["git", "init"],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
subprocess.run(
["git", "config", "user.email", "test@test.com"],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
subprocess.run(
["git", "config", "user.name", "Test"],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
Path(context.crud_tmpdir, fname).write_text(content, encoding="utf-8")
subprocess.run(
["git", "add", "."],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
subprocess.run(
["git", "commit", "-m", "init"],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
@given('a temp git sub-file "{fname}" containing "{content}"')
def step_given_temp_git_sub_file(context: Context, fname: str, content: str) -> None:
Path(context.crud_tmpdir, fname).write_text(content, encoding="utf-8")
subprocess.run(
["git", "add", "."],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
subprocess.run(
["git", "commit", "-m", f"add {fname}"],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
@given(
'a temp git sub-directory "{dirname}" with file "{fname}" containing "{content}"'
)
def step_given_temp_git_sub_dir(
context: Context, dirname: str, fname: str, content: str
) -> None:
dirpath = Path(context.crud_tmpdir, dirname)
dirpath.mkdir(parents=True, exist_ok=True)
(dirpath / fname).write_text(content, encoding="utf-8")
subprocess.run(
["git", "add", "."],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
subprocess.run(
["git", "commit", "-m", f"add {dirname}/{fname}"],
cwd=context.crud_tmpdir,
capture_output=True,
check=True,
)
@given("a git-checkout resource pointing to that repo")
def step_given_git_resource(context: Context) -> None:
context.crud_handler = GitCheckoutHandler()
context.crud_resource = _make_resource("git-checkout", context.crud_tmpdir)
# ---------------------------------------------------------------------------
# Given: database handler (NotImplementedError tests)
# ---------------------------------------------------------------------------
@given("a database resource handler")
def step_given_db_handler(context: Context) -> None:
context.crud_handler = DatabaseResourceHandler()
@given("a dummy database resource")
def step_given_dummy_db_resource(context: Context) -> None:
context.crud_resource = _make_resource(
"postgres", "/tmp/fake-db", rid="01KJ5C5TPMP8GGX3QC83E2MAQS"
)
# ---------------------------------------------------------------------------
# When: CRUD operations
# ---------------------------------------------------------------------------
@when('I call read on the fs-directory handler with path "{path}"')
@when('I call read on the git-checkout handler with path "{path}"')
def step_when_read(context: Context, path: str) -> None:
try:
context.crud_result = context.crud_handler.read(
resource=context.crud_resource, path=path
)
context.crud_error = None
except Exception as exc:
context.crud_result = None
context.crud_error = exc
@when("I call read on the fs-directory handler for the root")
def step_when_read_root(context: Context) -> None:
try:
context.crud_result = context.crud_handler.read(
resource=context.crud_resource, path=""
)
context.crud_error = None
except Exception as exc:
context.crud_result = None
context.crud_error = exc
@when("I call read on the database handler")
def step_when_read_db(context: Context) -> None:
try:
context.crud_result = context.crud_handler.read(resource=context.crud_resource)
context.crud_error = None
except Exception as exc:
context.crud_result = None
context.crud_error = exc
@when('I call write on the fs-directory handler with path "{path}" and data "{data}"')
@when('I call write on the git-checkout handler with path "{path}" and data "{data}"')
def step_when_write(context: Context, path: str, data: str) -> None:
try:
context.crud_result = context.crud_handler.write(
resource=context.crud_resource,
path=path,
data=data.encode("utf-8"),
)
context.crud_error = None
except Exception as exc:
context.crud_result = None
context.crud_error = exc
@when('I call write on the database handler with path "{path}" and data "{data}"')
def step_when_write_db(context: Context, path: str, data: str) -> None:
try:
context.crud_result = context.crud_handler.write(
resource=context.crud_resource,
path=path,
data=data.encode("utf-8"),
)
context.crud_error = None
except Exception as exc:
context.crud_result = None
context.crud_error = exc
@when('I call delete on the fs-directory handler with path "{path}"')
@when('I call delete on the git-checkout handler with path "{path}"')
def step_when_delete(context: Context, path: str) -> None:
try:
context.crud_result = context.crud_handler.delete(
resource=context.crud_resource, path=path
)
context.crud_error = None
except Exception as exc:
context.crud_result = None
context.crud_error = exc
@when("I call list_children on the fs-directory handler")
@when("I call list_children on the git-checkout handler")
def step_when_list_children(context: Context) -> None:
context.crud_result = context.crud_handler.list_children(
resource=context.crud_resource
)
@when("I call diff on the fs-directory handler against the second directory")
@when("I call diff on the git-checkout handler against the second directory")
def step_when_diff(context: Context) -> None:
context.crud_result = context.crud_handler.diff(
resource=context.crud_resource,
other_location=context.crud_tmpdir2,
)
@when("I call discover_children on the fs-directory handler")
@when("I call discover_children on the git-checkout handler")
def step_when_discover(context: Context) -> None:
context.crud_result = context.crud_handler.discover_children(
resource=context.crud_resource
)
# ---------------------------------------------------------------------------
# Then: assertions
# ---------------------------------------------------------------------------
@then('the content data should be "{expected}"')
def step_then_content_data(context: Context, expected: str) -> None:
assert context.crud_result is not None, "Expected Content, got None"
actual = context.crud_result.data.decode("utf-8").strip()
assert actual == expected, f"Expected '{expected}', got '{actual}'"
@then('the content encoding should be "{expected}"')
def step_then_content_encoding(context: Context, expected: str) -> None:
assert context.crud_result.encoding == expected
@then("the content hash should not be empty")
def step_then_content_hash_not_empty(context: Context) -> None:
assert context.crud_result.content_hash, "content_hash is empty"
@then('the content text should contain "{substring}"')
def step_then_content_text_contains(context: Context, substring: str) -> None:
text = context.crud_result.text
assert substring in text, f"'{substring}' not in '{text}'"
@then("a crud FileNotFoundError should be raised")
def step_then_crud_fnf_error(context: Context) -> None:
assert isinstance(context.crud_error, FileNotFoundError), (
f"Expected FileNotFoundError, got {type(context.crud_error)}"
)
@then("the write result should be successful")
def step_then_write_success(context: Context) -> None:
assert context.crud_result is not None
assert context.crud_result.success is True, (
f"Write failed: {context.crud_result.message}"
)
@then("the write result bytes_written should be {count:d}")
def step_then_write_bytes(context: Context, count: int) -> None:
assert context.crud_result.bytes_written == count
@then('the file "{path}" should exist in the temp directory with content "{expected}"')
def step_then_file_exists_with_content(
context: Context, path: str, expected: str
) -> None:
target = Path(context.crud_tmpdir) / path
assert target.exists(), f"{path} does not exist"
actual = target.read_text(encoding="utf-8")
assert actual == expected, f"Expected '{expected}', got '{actual}'"
@then('the file "{path}" should exist in the temp git repo with content "{expected}"')
def step_then_git_file_exists(context: Context, path: str, expected: str) -> None:
target = Path(context.crud_tmpdir) / path
assert target.exists(), f"{path} does not exist"
actual = target.read_text(encoding="utf-8")
assert actual == expected, f"Expected '{expected}', got '{actual}'"
@then("the delete result should be successful")
def step_then_delete_success(context: Context) -> None:
assert context.crud_result is not None
assert context.crud_result.success is True
@then('the file "{path}" should not exist in the temp directory')
@then('the file "{path}" should not exist in the temp git repo')
def step_then_file_not_exists(context: Context, path: str) -> None:
target = Path(context.crud_tmpdir) / path
assert not target.exists(), f"{path} still exists"
@then("the children list should equal {expected}")
def step_then_children_equal(context: Context, expected: str) -> None:
import ast
expected_list = ast.literal_eval(expected)
assert context.crud_result == expected_list, (
f"Expected {expected_list}, got {context.crud_result}"
)
@then('the children list should contain "{item}"')
def step_then_children_contains(context: Context, item: str) -> None:
assert item in context.crud_result, f"'{item}' not in {context.crud_result}"
@then("the diff result should have changes")
def step_then_diff_has_changes(context: Context) -> None:
assert context.crud_result.has_changes is True
@then("the diff result should have no changes")
def step_then_diff_no_changes(context: Context) -> None:
assert context.crud_result.has_changes is False
@then("the diff files_changed should be {count:d}")
def step_then_diff_files_changed(context: Context, count: int) -> None:
assert context.crud_result.files_changed == count
@then("the diff insertions should be greater than {count:d}")
def step_then_diff_insertions_gt(context: Context, count: int) -> None:
assert context.crud_result.insertions > count, (
f"insertions={context.crud_result.insertions} not > {count}"
)
@then("the discovered children should have {count:d} items")
def step_then_discover_count(context: Context, count: int) -> None:
assert len(context.crud_result) == count, (
f"Expected {count} children, got {len(context.crud_result)}"
)
@then('the discovered children names should include "{name}"')
def step_then_discover_name(context: Context, name: str) -> None:
names = [r.name for r in context.crud_result]
assert name in names, f"'{name}' not in {names}"
@then(
'a crud NotImplementedError should be raised with message containing "{fragment}"'
)
def step_then_crud_not_implemented(context: Context, fragment: str) -> None:
assert isinstance(context.crud_error, NotImplementedError), (
f"Expected NotImplementedError, got {type(context.crud_error)}"
)
assert fragment in str(context.crud_error), (
f"'{fragment}' not in '{context.crud_error}'"
)
@then("a crud PermissionError should be raised")
def step_then_crud_permission_error(context: Context) -> None:
assert isinstance(context.crud_error, PermissionError), (
f"Expected PermissionError, got {type(context.crud_error)}: {context.crud_error}"
)
@then("a crud read result should be returned without error")
def step_then_crud_read_result_no_error(context: Context) -> None:
assert context.crud_error is None, (
f"Expected no error, got {type(context.crud_error)}: {context.crud_error}"
)
assert context.crud_result is not None, "Expected a read result, got None"
@then("a crud write result should be returned without error")
def step_then_crud_write_result_no_error(context: Context) -> None:
assert context.crud_error is None, (
f"Expected no error, got {type(context.crud_error)}: {context.crud_error}"
)
assert context.crud_result is not None, "Expected a write result, got None"