"""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"