02250473ad
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 (commit9c6d6915) 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 (commit0d5d9cf0and 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 (commit1a07a891): - '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 (commit300a5d6d): - 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
299 lines
11 KiB
Python
299 lines
11 KiB
Python
"""Step definitions for resource_handler_sandbox.feature.
|
|
|
|
Tests sandbox lifecycle, checkpoint/rollback, and project_access for
|
|
GitCheckoutHandler and FsDirectoryHandler.
|
|
|
|
All step text uses the ``rh836-`` prefix to avoid collisions.
|
|
|
|
Issue #836: ResourceHandler sandbox and checkpoint methods.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.domain.models.core.resource import (
|
|
PhysVirt,
|
|
Resource,
|
|
ResourceCapabilities,
|
|
)
|
|
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
|
|
from cleveragents.infrastructure.sandbox.manager import SandboxManager
|
|
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] = []
|
|
|
|
_COUNTER = 0
|
|
_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
|
|
|
|
def _next_id() -> str:
|
|
"""Generate a valid 26-char Crockford Base32 ID for tests."""
|
|
global _COUNTER
|
|
_COUNTER += 1
|
|
n = _COUNTER + 0x836000
|
|
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) -> Resource:
|
|
return Resource(
|
|
resource_id=_next_id(),
|
|
resource_type_name=rtype,
|
|
classification=PhysVirt.PHYSICAL,
|
|
location=location,
|
|
capabilities=ResourceCapabilities(
|
|
readable=True, writable=True, sandboxable=True
|
|
),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given: temp directories and git repos
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('rh836- a temp directory with file "{fname}" containing "{content}"')
|
|
def step_rh836_temp_dir(context: Context, fname: str, content: str) -> None:
|
|
context.rh836_tmpdir = tempfile.mkdtemp(prefix="rh836_fs_")
|
|
Path(context.rh836_tmpdir, fname).write_text(content, encoding="utf-8")
|
|
|
|
|
|
@given("rh836- an fs-directory resource and sandbox manager")
|
|
def step_rh836_fs_resource_and_manager(context: Context) -> None:
|
|
context.rh836_handler = FsDirectoryHandler()
|
|
context.rh836_resource = _make_resource("fs-directory", context.rh836_tmpdir)
|
|
context.rh836_plan_id = _next_id()
|
|
factory = SandboxFactory()
|
|
context.rh836_sandbox_mgr = SandboxManager(factory=factory, cleanup_on_exit=False)
|
|
|
|
|
|
@given('rh836- a temp git repo with file "{fname}" containing "{content}"')
|
|
def step_rh836_temp_git_repo(context: Context, fname: str, content: str) -> None:
|
|
context.rh836_tmpdir = tempfile.mkdtemp(prefix="rh836_git_")
|
|
subprocess.run(
|
|
["git", "init"], cwd=context.rh836_tmpdir, capture_output=True, check=True
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "test@test.com"],
|
|
cwd=context.rh836_tmpdir,
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.name", "Test"],
|
|
cwd=context.rh836_tmpdir,
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
Path(context.rh836_tmpdir, fname).write_text(content, encoding="utf-8")
|
|
subprocess.run(
|
|
["git", "add", "."], cwd=context.rh836_tmpdir, capture_output=True, check=True
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "init"],
|
|
cwd=context.rh836_tmpdir,
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
|
|
|
|
@given("rh836- a git-checkout resource and sandbox manager")
|
|
def step_rh836_git_resource_and_manager(context: Context) -> None:
|
|
context.rh836_handler = GitCheckoutHandler()
|
|
context.rh836_resource = _make_resource("git-checkout", context.rh836_tmpdir)
|
|
context.rh836_plan_id = _next_id()
|
|
factory = SandboxFactory()
|
|
context.rh836_sandbox_mgr = SandboxManager(factory=factory, cleanup_on_exit=False)
|
|
|
|
|
|
@given("rh836- a database handler and dummy resource")
|
|
def step_rh836_db_handler(context: Context) -> None:
|
|
context.rh836_handler = DatabaseResourceHandler()
|
|
context.rh836_resource = _make_resource("postgres", "/tmp/fake-db")
|
|
context.rh836_plan_id = _next_id()
|
|
context.rh836_sandbox_mgr = MagicMock(spec=SandboxManager)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When: sandbox operations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("rh836- I call create_sandbox on the fs-directory handler")
|
|
@when("rh836- I call create_sandbox on the git-checkout handler")
|
|
def step_rh836_create_sandbox(context: Context) -> None:
|
|
context.rh836_sandbox_result = context.rh836_handler.create_sandbox(
|
|
resource=context.rh836_resource,
|
|
plan_id=context.rh836_plan_id,
|
|
sandbox_manager=context.rh836_sandbox_mgr,
|
|
)
|
|
|
|
|
|
@when("rh836- I call create_sandbox on the fs-directory handler again")
|
|
@when("rh836- I call create_sandbox on the git-checkout handler again")
|
|
def step_rh836_create_sandbox_again(context: Context) -> None:
|
|
context.rh836_sandbox_result_2 = context.rh836_handler.create_sandbox(
|
|
resource=context.rh836_resource,
|
|
plan_id=context.rh836_plan_id,
|
|
sandbox_manager=context.rh836_sandbox_mgr,
|
|
)
|
|
|
|
|
|
@when("rh836- I create a checkpoint on the fs-directory handler")
|
|
@when("rh836- I create a checkpoint on the git-checkout handler")
|
|
def step_rh836_create_checkpoint(context: Context) -> None:
|
|
context.rh836_checkpoint_result = context.rh836_handler.create_checkpoint(
|
|
resource=context.rh836_resource,
|
|
plan_id=context.rh836_plan_id,
|
|
sandbox_manager=context.rh836_sandbox_mgr,
|
|
)
|
|
|
|
|
|
@when('rh836- I modify the sandbox file "{fname}" to "{content}"')
|
|
def step_rh836_modify_sandbox_file(context: Context, fname: str, content: str) -> None:
|
|
sandbox_path = context.rh836_sandbox_result.sandbox_path
|
|
Path(sandbox_path, fname).write_text(content, encoding="utf-8")
|
|
|
|
|
|
@when("rh836- I rollback the fs-directory handler to the checkpoint")
|
|
@when("rh836- I rollback the git-checkout handler to the checkpoint")
|
|
def step_rh836_rollback(context: Context) -> None:
|
|
context.rh836_rollback_result = context.rh836_handler.rollback_to(
|
|
resource=context.rh836_resource,
|
|
plan_id=context.rh836_plan_id,
|
|
checkpoint_id=context.rh836_checkpoint_result.checkpoint_id,
|
|
sandbox_manager=context.rh836_sandbox_mgr,
|
|
)
|
|
|
|
|
|
@when('rh836- I check project_access for principal "{principal}" action "{action}"')
|
|
def step_rh836_check_access(context: Context, principal: str, action: str) -> None:
|
|
context.rh836_access_result = context.rh836_handler.project_access(
|
|
resource=context.rh836_resource,
|
|
principal=principal,
|
|
action=action,
|
|
)
|
|
|
|
|
|
@when("rh836- I call create_checkpoint on the database handler")
|
|
def step_rh836_db_checkpoint(context: Context) -> None:
|
|
try:
|
|
context.rh836_result = context.rh836_handler.create_checkpoint(
|
|
resource=context.rh836_resource,
|
|
plan_id=context.rh836_plan_id,
|
|
sandbox_manager=context.rh836_sandbox_mgr,
|
|
)
|
|
context.rh836_error = None
|
|
except Exception as exc:
|
|
context.rh836_error = exc
|
|
context.rh836_result = None
|
|
|
|
|
|
@when("rh836- I call rollback_to on the database handler")
|
|
def step_rh836_db_rollback(context: Context) -> None:
|
|
try:
|
|
context.rh836_result = context.rh836_handler.rollback_to(
|
|
resource=context.rh836_resource,
|
|
plan_id=context.rh836_plan_id,
|
|
checkpoint_id="fake-checkpoint",
|
|
sandbox_manager=context.rh836_sandbox_mgr,
|
|
)
|
|
context.rh836_error = None
|
|
except Exception as exc:
|
|
context.rh836_error = exc
|
|
context.rh836_result = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then: assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("rh836- the sandbox result should be created")
|
|
def step_rh836_sandbox_created(context: Context) -> None:
|
|
assert context.rh836_sandbox_result.created is True
|
|
|
|
|
|
@then("rh836- the sandbox result path should exist")
|
|
def step_rh836_sandbox_path_exists(context: Context) -> None:
|
|
assert Path(context.rh836_sandbox_result.sandbox_path).exists()
|
|
|
|
|
|
@then("rh836- the second sandbox result should not be created")
|
|
def step_rh836_second_not_created(context: Context) -> None:
|
|
assert context.rh836_sandbox_result_2.created is False
|
|
|
|
|
|
@then("rh836- both sandbox results should have the same sandbox_id")
|
|
def step_rh836_same_sandbox_id(context: Context) -> None:
|
|
assert (
|
|
context.rh836_sandbox_result.sandbox_id
|
|
== context.rh836_sandbox_result_2.sandbox_id
|
|
)
|
|
|
|
|
|
@then("rh836- the rollback result should be successful")
|
|
def step_rh836_rollback_success(context: Context) -> None:
|
|
assert context.rh836_rollback_result.success is True, (
|
|
f"Rollback failed: {context.rh836_rollback_result.message}"
|
|
)
|
|
|
|
|
|
@then('rh836- the sandbox file "{fname}" should contain "{expected}"')
|
|
def step_rh836_sandbox_file_contains(
|
|
context: Context, fname: str, expected: str
|
|
) -> None:
|
|
sandbox_path = context.rh836_sandbox_result.sandbox_path
|
|
actual = Path(sandbox_path, fname).read_text(encoding="utf-8").strip()
|
|
assert actual == expected, f"Expected '{expected}', got '{actual}'"
|
|
|
|
|
|
@then("rh836- the access result should be permitted")
|
|
def step_rh836_access_permitted(context: Context) -> None:
|
|
assert context.rh836_access_result.permitted is True
|
|
|
|
|
|
@then('rh836- the access result reason should contain "{fragment}"')
|
|
def step_rh836_access_reason(context: Context, fragment: str) -> None:
|
|
assert fragment in context.rh836_access_result.reason, (
|
|
f"'{fragment}' not in '{context.rh836_access_result.reason}'"
|
|
)
|
|
|
|
|
|
@then('rh836- a NotImplementedError should be raised containing "{fragment}"')
|
|
def step_rh836_not_implemented(context: Context, fragment: str) -> None:
|
|
assert isinstance(context.rh836_error, NotImplementedError), (
|
|
f"Expected NotImplementedError, got {type(context.rh836_error)}"
|
|
)
|
|
assert fragment in str(context.rh836_error), (
|
|
f"'{fragment}' not in '{context.rh836_error}'"
|
|
)
|
|
|
|
|
|
@then("rh836- a checkpoint result should be returned without error")
|
|
def step_rh836_checkpoint_result_no_error(context: Context) -> None:
|
|
assert context.rh836_error is None, (
|
|
f"Expected no error, got {type(context.rh836_error)}: {context.rh836_error}"
|
|
)
|
|
assert context.rh836_result is not None, "Expected a checkpoint result, got None"
|
|
|
|
|
|
@then("rh836- a rollback result should be returned without error")
|
|
def step_rh836_rollback_result_no_error(context: Context) -> None:
|
|
assert context.rh836_error is None, (
|
|
f"Expected no error, got {type(context.rh836_error)}: {context.rh836_error}"
|
|
)
|
|
assert context.rh836_result is not None, "Expected a rollback result, got None"
|