Files
placeholder/features/steps/fs_directory_coverage_steps.py
freemo 31472b5413 test(coverage): add Behave scenarios for 39 under-covered modules
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate.

ISSUES CLOSED: #1232
2026-03-31 21:47:12 +00:00

526 lines
20 KiB
Python

"""Step definitions for fs_directory_coverage.feature.
Exercises uncovered lines in fs_directory.py:
L133 — delete() with empty path raises PermissionError
L143 — delete() removes subdirectory via shutil.rmtree
L197 — diff() with file missing from resource dir (a_lines = [])
L204 — diff() with file missing from other dir (b_lines = [])
L287-289 — create_checkpoint() no sandbox RuntimeError
L329-331 — rollback_to() no sandbox RuntimeError
L336-339 — rollback_to() missing/expired checkpoint
L347,349 — rollback_to() .git skip + rmtree in sandbox clear
L357,360 — rollback_to() .git skip + copytree in snapshot restore
L374-381 — discard_checkpoints()
"""
from __future__ import annotations
import shutil
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.resource.handlers.fs_directory import FsDirectoryHandler
__all__: list[str] = []
_ULID_COUNTER = 900_000
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(location: str) -> Resource:
return Resource(
resource_id=_next_ulid(),
resource_type_name="fs-directory",
classification=PhysVirt.PHYSICAL,
location=location,
capabilities=ResourceCapabilities(
readable=True, writable=True, sandboxable=True
),
)
# ---------------------------------------------------------------------------
# Delete with empty path — L133
# ---------------------------------------------------------------------------
@given('fsdcov a temp directory with a file "{fname}"')
def step_fsdcov_given_temp_dir_file(context: Context, fname: str) -> None:
d = tempfile.mkdtemp(prefix="fsdcov_del_")
context.add_cleanup(lambda: shutil.rmtree(d, ignore_errors=True))
Path(d, fname).write_text("content", encoding="utf-8")
context.fsdcov_handler = FsDirectoryHandler()
context.fsdcov_resource = _make_resource(d)
context.fsdcov_tmpdir = d
@when("fsdcov I delete with an empty path")
def step_fsdcov_delete_empty_path(context: Context) -> None:
handler: FsDirectoryHandler = context.fsdcov_handler
try:
handler.delete(resource=context.fsdcov_resource, path="")
context.fsdcov_error = None
except Exception as exc:
context.fsdcov_error = exc
@then('fsdcov a PermissionError is raised with message "{fragment}"')
def step_fsdcov_permission_error(context: Context, fragment: str) -> None:
assert context.fsdcov_error is not None, "Expected an error but none was raised"
assert isinstance(context.fsdcov_error, PermissionError), (
f"Expected PermissionError, got {type(context.fsdcov_error).__name__}"
)
assert fragment in str(context.fsdcov_error), (
f"Expected '{fragment}' in '{context.fsdcov_error}'"
)
# ---------------------------------------------------------------------------
# Delete subdirectory — L143
# ---------------------------------------------------------------------------
@given('fsdcov a temp directory with a subdirectory "{dirname}" containing "{fname}"')
def step_fsdcov_given_temp_dir_subdir(
context: Context, dirname: str, fname: str
) -> None:
d = tempfile.mkdtemp(prefix="fsdcov_delsub_")
context.add_cleanup(lambda: shutil.rmtree(d, ignore_errors=True))
sub = Path(d, dirname)
sub.mkdir(parents=True, exist_ok=True)
(sub / fname).write_text("inner content", encoding="utf-8")
context.fsdcov_handler = FsDirectoryHandler()
context.fsdcov_resource = _make_resource(d)
context.fsdcov_tmpdir = d
@when('fsdcov I delete subdirectory "{dirname}"')
def step_fsdcov_delete_subdir(context: Context, dirname: str) -> None:
handler: FsDirectoryHandler = context.fsdcov_handler
try:
context.fsdcov_delete_result = handler.delete(
resource=context.fsdcov_resource, path=dirname
)
context.fsdcov_error = None
except Exception as exc:
context.fsdcov_error = exc
context.fsdcov_delete_result = None
@then("fsdcov the delete result is successful")
def step_fsdcov_delete_success(context: Context) -> None:
assert context.fsdcov_error is None, f"Unexpected error: {context.fsdcov_error}"
assert context.fsdcov_delete_result is not None
assert context.fsdcov_delete_result.success is True
@then('fsdcov the subdirectory "{dirname}" no longer exists')
def step_fsdcov_subdir_gone(context: Context, dirname: str) -> None:
target = Path(context.fsdcov_tmpdir) / dirname
assert not target.exists(), (
f"Expected '{dirname}' to be removed, but it still exists"
)
# ---------------------------------------------------------------------------
# Diff with files on only one side — L197, L204
# ---------------------------------------------------------------------------
@given('fsdcov a temp directory "{label}" with no files')
def step_fsdcov_given_empty_dir(context: Context, label: str) -> None:
d = tempfile.mkdtemp(prefix=f"fsdcov_{label}_")
context.add_cleanup(lambda: shutil.rmtree(d, ignore_errors=True))
if not hasattr(context, "fsdcov_diff_dirs"):
context.fsdcov_diff_dirs = {}
context.fsdcov_diff_dirs[label] = d
@given('fsdcov a temp directory "{label}" with file "{fname}" containing "{content}"')
def step_fsdcov_given_dir_with_file(
context: Context, label: str, fname: str, content: str
) -> None:
d = tempfile.mkdtemp(prefix=f"fsdcov_{label}_")
context.add_cleanup(lambda: shutil.rmtree(d, ignore_errors=True))
Path(d, fname).write_text(content, encoding="utf-8")
if not hasattr(context, "fsdcov_diff_dirs"):
context.fsdcov_diff_dirs = {}
context.fsdcov_diff_dirs[label] = d
@when("fsdcov I diff dir_a against dir_b")
def step_fsdcov_diff(context: Context) -> None:
handler = FsDirectoryHandler()
dir_a = context.fsdcov_diff_dirs["dir_a"]
dir_b = context.fsdcov_diff_dirs["dir_b"]
resource = _make_resource(dir_a)
context.fsdcov_diff_result = handler.diff(resource=resource, other_location=dir_b)
@then("fsdcov the diff shows changes with insertions")
def step_fsdcov_diff_insertions(context: Context) -> None:
r = context.fsdcov_diff_result
assert r.has_changes is True, "Expected diff to have changes"
assert r.insertions > 0, f"Expected insertions > 0, got {r.insertions}"
@then("fsdcov the diff shows changes with deletions")
def step_fsdcov_diff_deletions(context: Context) -> None:
r = context.fsdcov_diff_result
assert r.has_changes is True, "Expected diff to have changes"
assert r.deletions > 0, f"Expected deletions > 0, got {r.deletions}"
# ---------------------------------------------------------------------------
# create_checkpoint with no sandbox — L287-289
# ---------------------------------------------------------------------------
@given("fsdcov a handler and a resource with location")
def step_fsdcov_handler_and_resource(context: Context) -> None:
d = tempfile.mkdtemp(prefix="fsdcov_ckpt_")
context.add_cleanup(lambda: shutil.rmtree(d, ignore_errors=True))
context.fsdcov_handler = FsDirectoryHandler()
context.fsdcov_resource = _make_resource(d)
context.fsdcov_tmpdir = d
@given("fsdcov a sandbox manager that returns None for get_sandbox")
def step_fsdcov_sandbox_mgr_none(context: Context) -> None:
mgr = MagicMock()
mgr.get_sandbox.return_value = None
context.fsdcov_sandbox_manager = mgr
@given("fsdcov a sandbox manager that returns sandbox with no context")
def step_fsdcov_sandbox_mgr_no_context(context: Context) -> None:
mgr = MagicMock()
sandbox_mock = MagicMock()
sandbox_mock.context = None
mgr.get_sandbox.return_value = sandbox_mock
context.fsdcov_sandbox_manager = mgr
@when("fsdcov I call create_checkpoint")
def step_fsdcov_call_create_checkpoint(context: Context) -> None:
handler: FsDirectoryHandler = context.fsdcov_handler
try:
handler.create_checkpoint(
resource=context.fsdcov_resource,
plan_id="test-plan-1",
sandbox_manager=context.fsdcov_sandbox_manager,
)
context.fsdcov_error = None
except Exception as exc:
context.fsdcov_error = exc
@then('fsdcov a RuntimeError is raised with message "{fragment}"')
def step_fsdcov_runtime_error(context: Context, fragment: str) -> None:
assert context.fsdcov_error is not None, "Expected an error but none was raised"
assert isinstance(context.fsdcov_error, RuntimeError), (
f"Expected RuntimeError, got {type(context.fsdcov_error).__name__}"
)
assert fragment in str(context.fsdcov_error), (
f"Expected '{fragment}' in '{context.fsdcov_error}'"
)
# ---------------------------------------------------------------------------
# rollback_to with no sandbox — L329-331
# ---------------------------------------------------------------------------
@when('fsdcov I call rollback_to with checkpoint "{ckpt_id}"')
def step_fsdcov_rollback_to(context: Context, ckpt_id: str) -> None:
handler: FsDirectoryHandler = context.fsdcov_handler
try:
context.fsdcov_rollback_result = handler.rollback_to(
resource=context.fsdcov_resource,
plan_id="test-plan-1",
checkpoint_id=ckpt_id,
sandbox_manager=context.fsdcov_sandbox_manager,
)
context.fsdcov_error = None
except Exception as exc:
context.fsdcov_error = exc
context.fsdcov_rollback_result = None
# ---------------------------------------------------------------------------
# rollback_to with missing checkpoint — L336-339
# ---------------------------------------------------------------------------
@given("fsdcov a sandbox manager with a valid sandbox")
def step_fsdcov_sandbox_mgr_valid(context: Context) -> None:
sandbox_path = tempfile.mkdtemp(prefix="fsdcov_sb_")
context.add_cleanup(lambda: shutil.rmtree(sandbox_path, ignore_errors=True))
mgr = MagicMock()
sandbox_mock = MagicMock()
ctx_mock = MagicMock()
ctx_mock.sandbox_path = sandbox_path
sandbox_mock.context = ctx_mock
mgr.get_sandbox.return_value = sandbox_mock
context.fsdcov_sandbox_manager = mgr
context.fsdcov_sandbox_path = sandbox_path
@then('fsdcov the rollback result is unsuccessful with message "{fragment}"')
def step_fsdcov_rollback_unsuccessful(context: Context, fragment: str) -> None:
assert context.fsdcov_error is None, f"Unexpected exception: {context.fsdcov_error}"
r = context.fsdcov_rollback_result
assert r is not None, "Expected a rollback result"
assert r.success is False, f"Expected success=False, got {r.success}"
assert fragment in r.message, f"Expected '{fragment}' in '{r.message}'"
# ---------------------------------------------------------------------------
# rollback_to with expired checkpoint path — L336-339
# ---------------------------------------------------------------------------
@given("fsdcov a handler with a checkpoint pointing to a deleted path")
def step_fsdcov_handler_expired_ckpt(context: Context) -> None:
d = tempfile.mkdtemp(prefix="fsdcov_expired_")
context.add_cleanup(lambda: shutil.rmtree(d, ignore_errors=True))
context.fsdcov_handler = FsDirectoryHandler()
context.fsdcov_resource = _make_resource(d)
# Create a temp dir, record it as a checkpoint, then delete it
expired_dir = tempfile.mkdtemp(prefix="fsdcov_snap_expired_")
context.fsdcov_expired_ckpt_id = "expired-ckpt-xyz"
context.fsdcov_handler._checkpoints[context.fsdcov_expired_ckpt_id] = expired_dir
shutil.rmtree(expired_dir)
@when("fsdcov I call rollback_to with the expired checkpoint")
def step_fsdcov_rollback_expired(context: Context) -> None:
handler: FsDirectoryHandler = context.fsdcov_handler
try:
context.fsdcov_rollback_result = handler.rollback_to(
resource=context.fsdcov_resource,
plan_id="test-plan-1",
checkpoint_id=context.fsdcov_expired_ckpt_id,
sandbox_manager=context.fsdcov_sandbox_manager,
)
context.fsdcov_error = None
except Exception as exc:
context.fsdcov_error = exc
context.fsdcov_rollback_result = None
# ---------------------------------------------------------------------------
# rollback_to with .git skip and directory handling — L347,349,357,360
# ---------------------------------------------------------------------------
@given("fsdcov a handler with a real checkpoint snapshot")
def step_fsdcov_handler_real_checkpoint(context: Context) -> None:
# Create snapshot directory with: .git/, a subdir, and a file
snap_parent = tempfile.mkdtemp(prefix="fsdcov_snap_")
context.add_cleanup(lambda: shutil.rmtree(snap_parent, ignore_errors=True))
snap_path = Path(snap_parent) / "snapshot"
snap_path.mkdir()
# .git directory in snapshot (should be skipped during restore)
(snap_path / ".git").mkdir()
(snap_path / ".git" / "HEAD").write_text("ref: refs/heads/main\n")
# A subdirectory to restore (exercises copytree — L360)
(snap_path / "src").mkdir()
(snap_path / "src" / "main.py").write_text("print('hello')\n")
# A regular file to restore (exercises copy2)
(snap_path / "README.md").write_text("# Project\n")
context.fsdcov_handler = FsDirectoryHandler()
# Register the snapshot as a checkpoint on the handler
context.fsdcov_real_ckpt_id = "real-ckpt-001"
context.fsdcov_handler._checkpoints[context.fsdcov_real_ckpt_id] = str(snap_path)
# Create a resource location
res_dir = tempfile.mkdtemp(prefix="fsdcov_resdir_")
context.add_cleanup(lambda: shutil.rmtree(res_dir, ignore_errors=True))
context.fsdcov_resource = _make_resource(res_dir)
@given("fsdcov a sandbox with .git dir and other content")
def step_fsdcov_sandbox_with_git(context: Context) -> None:
sandbox_path = tempfile.mkdtemp(prefix="fsdcov_sb_git_")
context.add_cleanup(lambda: shutil.rmtree(sandbox_path, ignore_errors=True))
sb = Path(sandbox_path)
# .git directory — must be preserved (L347)
(sb / ".git").mkdir()
(sb / ".git" / "config").write_text("[core]\n")
# A subdirectory — must be removed by rmtree (L349)
(sb / "old_dir").mkdir()
(sb / "old_dir" / "old.py").write_text("old\n")
# A regular file — must be removed by unlink (L351)
(sb / "stale.txt").write_text("stale\n")
mgr = MagicMock()
sandbox_mock = MagicMock()
ctx_mock = MagicMock()
ctx_mock.sandbox_path = sandbox_path
sandbox_mock.context = ctx_mock
mgr.get_sandbox.return_value = sandbox_mock
context.fsdcov_sandbox_manager = mgr
context.fsdcov_sandbox_path = sandbox_path
@when("fsdcov I call rollback_to with the real checkpoint")
def step_fsdcov_rollback_real(context: Context) -> None:
handler: FsDirectoryHandler = context.fsdcov_handler
try:
context.fsdcov_rollback_result = handler.rollback_to(
resource=context.fsdcov_resource,
plan_id="test-plan-1",
checkpoint_id=context.fsdcov_real_ckpt_id,
sandbox_manager=context.fsdcov_sandbox_manager,
)
context.fsdcov_error = None
except Exception as exc:
context.fsdcov_error = exc
context.fsdcov_rollback_result = None
@then("fsdcov the rollback result is successful")
def step_fsdcov_rollback_success(context: Context) -> None:
assert context.fsdcov_error is None, f"Unexpected error: {context.fsdcov_error}"
r = context.fsdcov_rollback_result
assert r is not None, "Expected a rollback result"
assert r.success is True, f"Expected success=True, got {r.success}"
@then("fsdcov the .git directory is preserved in the sandbox")
def step_fsdcov_git_preserved(context: Context) -> None:
sb = Path(context.fsdcov_sandbox_path)
assert (sb / ".git").exists(), ".git directory was removed but should be preserved"
assert (sb / ".git" / "config").exists(), (
".git/config was removed but should be preserved"
)
@then("fsdcov the snapshot content is restored in the sandbox")
def step_fsdcov_snapshot_restored(context: Context) -> None:
sb = Path(context.fsdcov_sandbox_path)
# Subdirectory from snapshot should be restored (L360)
assert (sb / "src").is_dir(), "Snapshot 'src/' directory was not restored"
assert (sb / "src" / "main.py").exists(), "Snapshot 'src/main.py' was not restored"
assert (sb / "src" / "main.py").read_text() == "print('hello')\n"
# File from snapshot should be restored
assert (sb / "README.md").exists(), "Snapshot 'README.md' was not restored"
assert (sb / "README.md").read_text() == "# Project\n"
# Old content should be gone
assert not (sb / "old_dir").exists(), "Old sandbox content was not cleared"
assert not (sb / "stale.txt").exists(), "Old sandbox file was not cleared"
# ---------------------------------------------------------------------------
# discard_checkpoints — L374-381
# ---------------------------------------------------------------------------
@given('fsdcov a handler with two checkpoints for plan "{plan_id}"')
def step_fsdcov_two_checkpoints(context: Context, plan_id: str) -> None:
context.fsdcov_handler = FsDirectoryHandler()
context.fsdcov_plan_id = plan_id
# Create two temp snapshot dirs and register them
for i in range(2):
snap_dir = tempfile.mkdtemp(prefix=f"fsdcov_disc_{i}_")
snap_path = Path(snap_dir) / "snapshot"
snap_path.mkdir()
(snap_path / f"file{i}.txt").write_text(f"data{i}")
ckpt_id = f"checkpoint-{plan_id}-2024010{i}"
context.fsdcov_handler._checkpoints[ckpt_id] = str(snap_path)
# Track parent dirs for cleanup verification
context.fsdcov_discard_snap_parents = [
str(Path(v).parent) for v in context.fsdcov_handler._checkpoints.values()
]
for p in context.fsdcov_discard_snap_parents:
context.add_cleanup(lambda pp=p: shutil.rmtree(pp, ignore_errors=True))
@when('fsdcov I call discard_checkpoints with plan_id "{plan_id}"')
def step_fsdcov_discard_checkpoints(context: Context, plan_id: str) -> None:
context.fsdcov_discard_count = context.fsdcov_handler.discard_checkpoints(
plan_id=plan_id
)
@then("fsdcov discard_checkpoints returns {count:d}")
def step_fsdcov_discard_returns(context: Context, count: int) -> None:
assert context.fsdcov_discard_count == count, (
f"Expected {count}, got {context.fsdcov_discard_count}"
)
@then('fsdcov no checkpoints remain for plan "{plan_id}"')
def step_fsdcov_no_checkpoints_for_plan(context: Context, plan_id: str) -> None:
remaining = [cid for cid in context.fsdcov_handler._checkpoints if plan_id in cid]
assert remaining == [], (
f"Expected no checkpoints for '{plan_id}', found {remaining}"
)
@given("fsdcov a handler with checkpoints for different plans")
def step_fsdcov_multi_plan_checkpoints(context: Context) -> None:
context.fsdcov_handler = FsDirectoryHandler()
for plan_name in ("plan-x", "plan-y"):
snap_dir = tempfile.mkdtemp(prefix=f"fsdcov_mp_{plan_name}_")
snap_path = Path(snap_dir) / "snapshot"
snap_path.mkdir()
(snap_path / "f.txt").write_text("data")
ckpt_id = f"checkpoint-{plan_name}-20240101"
context.fsdcov_handler._checkpoints[ckpt_id] = str(snap_path)
context.add_cleanup(lambda d=snap_dir: shutil.rmtree(d, ignore_errors=True))
@when("fsdcov I call discard_checkpoints with empty plan_id")
def step_fsdcov_discard_all(context: Context) -> None:
context.fsdcov_discard_count = context.fsdcov_handler.discard_checkpoints(
plan_id=""
)
@then("fsdcov all checkpoints are removed")
def step_fsdcov_all_removed(context: Context) -> None:
assert len(context.fsdcov_handler._checkpoints) == 0, (
f"Expected 0 checkpoints, found {len(context.fsdcov_handler._checkpoints)}"
)
assert context.fsdcov_discard_count >= 2, (
f"Expected at least 2 removed, got {context.fsdcov_discard_count}"
)