Files
cleveragents-core/features/steps/tui_permissions_screen_steps.py
HAL9000 f2b23e397f fix(tui): fix format check and undefined step in execution_environment feature
Applied ruff format fix to tui_permissions_screen_steps.py and corrected the step text mismatch in execution_environment.feature where 'it should not contain' was not updated to 'the container types should not contain' when the step definition was renamed.

ISSUES CLOSED: #10488
2026-06-06 12:11:44 -04:00

685 lines
23 KiB
Python

"""Step definitions for tui_permissions_screen.feature.
Tests cover:
- DiffDisplayMode, FileChangeType, PermissionDecision enums
- PermissionRequest model (unified/split/auto diffs)
- ToolPermissionRequest model (decisions, is_pending, is_allowed, is_rejected)
- PermissionRequestService (queue, decisions, session-scoped decisions)
- PermissionsScreen widget (load, navigate, cycle mode, allow/reject)
- Rendering helpers (_render_file_list, _render_diff_panel, _render_status_bar)
"""
from __future__ import annotations
from behave import given, then, when
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_permission_request(path: str, before: str, after: str):
from cleveragents.tui.permissions.models import FileChangeType, PermissionRequest
return PermissionRequest(
path=path,
change_type=FileChangeType.MODIFIED,
before_content=before,
after_content=after,
)
def _make_tool_request(request_id: str, tool_name: str, n_changes: int = 3):
from cleveragents.tui.permissions.models import (
FileChangeType,
PermissionRequest,
ToolPermissionRequest,
)
changes = [
PermissionRequest(
path=f"file{i}.py",
change_type=FileChangeType.MODIFIED,
before_content=f"old content {i}\nline2\n",
after_content=f"new content {i}\nline2\n",
)
for i in range(n_changes)
]
return ToolPermissionRequest(
request_id=request_id,
tool_name=tool_name,
resource_name="local/api-service",
changes=changes,
)
# ---------------------------------------------------------------------------
# DiffDisplayMode enum
# ---------------------------------------------------------------------------
@when("I inspect the DiffDisplayMode enum")
def step_inspect_diff_display_mode(context):
from cleveragents.tui.permissions.models import DiffDisplayMode
context._diff_display_mode = DiffDisplayMode
@then('DiffDisplayMode should have values "unified", "split", "auto"')
def step_diff_display_mode_values(context):
from cleveragents.tui.permissions.models import DiffDisplayMode
assert DiffDisplayMode.UNIFIED == "unified"
assert DiffDisplayMode.SPLIT == "split"
assert DiffDisplayMode.AUTO == "auto"
# ---------------------------------------------------------------------------
# FileChangeType enum
# ---------------------------------------------------------------------------
@when("I inspect the FileChangeType enum")
def step_inspect_file_change_type(context):
from cleveragents.tui.permissions.models import FileChangeType
context._file_change_type = FileChangeType
@then('FileChangeType should have values "M", "A", "D"')
def step_file_change_type_values(context):
from cleveragents.tui.permissions.models import FileChangeType
assert FileChangeType.MODIFIED == "M"
assert FileChangeType.ADDED == "A"
assert FileChangeType.DELETED == "D"
# ---------------------------------------------------------------------------
# PermissionDecision enum
# ---------------------------------------------------------------------------
@when("I inspect the PermissionDecision enum")
def step_inspect_permission_decision(context):
from cleveragents.tui.permissions.models import PermissionDecision
context._permission_decision = PermissionDecision
@then(
'PermissionDecision should have values "allow_once", "allow_always", "reject_once", "reject_always"'
)
def step_permission_decision_values(context):
from cleveragents.tui.permissions.models import PermissionDecision
assert PermissionDecision.ALLOW_ONCE == "allow_once"
assert PermissionDecision.ALLOW_ALWAYS == "allow_always"
assert PermissionDecision.REJECT_ONCE == "reject_once"
assert PermissionDecision.REJECT_ALWAYS == "reject_always"
# ---------------------------------------------------------------------------
# PermissionRequest model
# ---------------------------------------------------------------------------
_BEFORE = "def login(self, request):\n return self.authenticate(request)\n"
_AFTER = (
"def login(self, request):\n"
" result = self.authenticate(request)\n"
" self.log_attempt(request, result)\n"
" return result\n"
)
@given('a PermissionRequest for "{path}" with before and after content')
def step_create_permission_request(context, path):
context._perm_request = _make_permission_request(path, _BEFORE, _AFTER)
@given('a PermissionRequest for "{path}" with identical before and after content')
def step_create_identical_permission_request(context, path):
same = "def foo():\n pass\n"
context._perm_request = _make_permission_request(path, same, same)
@when('I render the diff in "{mode}" mode')
def step_render_diff(context, mode):
from cleveragents.tui.permissions.models import DiffDisplayMode
mode_enum = DiffDisplayMode(mode)
context._rendered_diff = context._perm_request.render_diff(mode_enum)
@then('the rendered diff should contain "{text}"')
def step_rendered_diff_contains(context, text):
assert text in context._rendered_diff, (
f"Expected {text!r} in diff:\n{context._rendered_diff}"
)
@then("the rendered diff should be empty")
def step_rendered_diff_empty(context):
assert context._rendered_diff == "", (
f"Expected empty diff, got:\n{context._rendered_diff}"
)
@when("I call split_diff on the request")
def step_call_split_diff(context):
context._sbs_left, context._sbs_right = context._perm_request.split_diff()
@then("both sides should have the same number of lines")
def step_split_same_length(context):
assert len(context._sbs_left) == len(context._sbs_right), (
f"Left: {len(context._sbs_left)}, Right: {len(context._sbs_right)}"
)
# ---------------------------------------------------------------------------
# ToolPermissionRequest model
# ---------------------------------------------------------------------------
@given("a ToolPermissionRequest with no decision")
def step_create_tool_permission_request(context):
context._tool_request = _make_tool_request("req-0", "local/file-write")
context._original_request = context._tool_request
@then("the request should be pending")
def step_request_is_pending(context):
assert context._tool_request.is_pending
@then("the request should not be allowed")
def step_request_not_allowed(context):
assert not context._tool_request.is_allowed
@then("the request should not be rejected")
def step_request_not_rejected(context):
assert not context._tool_request.is_rejected
@when('I apply decision "{decision}" to the request')
def step_apply_decision(context, decision):
from cleveragents.tui.permissions.models import PermissionDecision
context._decided_request = context._tool_request.apply_decision(
PermissionDecision(decision)
)
# Update context._tool_request to the decided version for subsequent steps
context._tool_request = context._decided_request
@then("the request should be allowed")
def step_request_is_allowed(context):
assert context._tool_request.is_allowed
@then("the request should be rejected")
def step_request_is_rejected(context):
assert context._tool_request.is_rejected
@then("the request should not be pending")
def step_request_not_pending(context):
assert not context._tool_request.is_pending
@then("the original request should still be pending")
def step_original_request_still_pending(context):
assert context._original_request.is_pending
# ---------------------------------------------------------------------------
# PermissionRequestService
# ---------------------------------------------------------------------------
@given("a fresh PermissionRequestService")
def step_create_service(context):
from cleveragents.tui.permissions.service import PermissionRequestService
context._service = PermissionRequestService()
context._clear_result = None
context._decision_result = None
@when('I enqueue a request with id "{request_id}" for tool "{tool_name}"')
def step_enqueue_request(context, request_id, tool_name):
req = _make_tool_request(request_id, tool_name)
context._service.enqueue(req)
context._last_request_id = request_id
@when('I record decision "{decision}" for request "{request_id}"')
def step_record_decision(context, decision, request_id):
from cleveragents.tui.permissions.models import PermissionDecision
context._decision_result = context._service.record_decision(
request_id, PermissionDecision(decision)
)
@then("the service should have {count:d} pending request")
def step_service_pending_count_singular(context, count):
assert len(context._service.pending_requests()) == count, (
f"Expected {count} pending, got {len(context._service.pending_requests())}"
)
@then("the service should have {count:d} pending requests")
def step_service_pending_count(context, count):
assert len(context._service.pending_requests()) == count, (
f"Expected {count} pending, got {len(context._service.pending_requests())}"
)
@then("the service should have {count:d} total requests")
def step_service_total_count(context, count):
assert len(context._service.all_requests()) == count, (
f"Expected {count} total, got {len(context._service.all_requests())}"
)
@then('the request "{request_id}" should have decision "{decision}"')
def step_request_has_decision(context, request_id, decision):
req = context._service.get(request_id)
assert req is not None, f"Request {request_id!r} not found"
assert req.decision is not None
assert req.decision == decision, f"Expected {decision!r}, got {req.decision!r}"
@then('the session decision for "{tool_name}" should be None')
def step_session_decision_none(context, tool_name):
result = context._service.get_session_decision(tool_name)
assert result is None, f"Expected None, got {result!r}"
@then('the session decision for "{tool_name}" should be "{decision}"')
def step_session_decision_value(context, tool_name, decision):
result = context._service.get_session_decision(tool_name)
assert result == decision, f"Expected {decision!r}, got {result!r}"
@when('I clear the session decision for "{tool_name}"')
def step_clear_session_decision(context, tool_name):
context._clear_result = context._service.clear_session_decision(tool_name)
@then("the clear result should be False")
def step_clear_result_false(context):
assert context._clear_result is False
@then("the decision result should be None")
def step_decision_result_none(context):
assert context._decision_result is None
@then('getting request "{request_id}" should return None')
def step_get_request_none(context, request_id):
result = context._service.get(request_id)
assert result is None
@when("I clear all requests")
def step_clear_all(context):
context._service.clear_all()
# ---------------------------------------------------------------------------
# PermissionsScreen widget
# ---------------------------------------------------------------------------
@given("a fresh PermissionsScreen")
def step_create_permissions_screen(context):
from cleveragents.tui.permissions.screen import PermissionsScreen
context._screen = PermissionsScreen()
@when("I load a ToolPermissionRequest with {n:d} file changes")
def step_load_tool_request(context, n):
req = _make_tool_request("req-screen", "local/file-write", n_changes=n)
context._screen.load_request(req)
@then('the screen text should contain "{text}"')
def step_screen_text_contains(context, text):
assert text in context._screen._text, (
f"Expected {text!r} in screen text:\n{context._screen._text}"
)
@then("the screen text should be empty")
def step_screen_text_empty(context):
assert context._screen._text == "", (
f"Expected empty screen text, got:\n{context._screen._text}"
)
@then("the selected index should be {index:d}")
def step_selected_index(context, index):
assert context._screen.selected_index == index, (
f"Expected index {index}, got {context._screen.selected_index}"
)
@when("I navigate next on the screen")
def step_navigate_next(context):
context._screen.navigate_next()
@when("I navigate prev on the screen")
def step_navigate_prev(context):
context._screen.navigate_prev()
@then('the diff mode should be "{mode}"')
def step_diff_mode(context, mode):
assert context._screen.diff_mode == mode, (
f"Expected mode {mode!r}, got {context._screen.diff_mode!r}"
)
@when("I cycle the diff mode")
def step_cycle_diff_mode(context):
context._screen.cycle_diff_mode()
@when('I set the diff mode to "{mode}"')
def step_set_diff_mode(context, mode):
from cleveragents.tui.permissions.models import DiffDisplayMode
context._screen.set_diff_mode(DiffDisplayMode(mode))
@when("I press allow_once on the screen")
def step_press_allow_once(context):
context._screen.allow_once()
@when("I press allow_always on the screen")
def step_press_allow_always(context):
context._screen.allow_always()
@when("I press reject_once on the screen")
def step_press_reject_once(context):
context._screen.reject_once()
@when("I press reject_always on the screen")
def step_press_reject_always(context):
context._screen.reject_always()
@then('the screen decision should be "{decision}"')
def step_screen_decision(context, decision):
assert context._screen.decision == decision, (
f"Expected decision {decision!r}, got {context._screen.decision!r}"
)
@then("the screen decision should be None")
def step_screen_decision_none(context):
assert context._screen.decision is None, (
f"Expected None, got {context._screen.decision!r}"
)
@when("I clear the screen")
def step_clear_screen(context):
context._screen.clear()
# ---------------------------------------------------------------------------
# _next_diff_mode helper
# ---------------------------------------------------------------------------
@when('I call _next_diff_mode with "{mode}"')
def step_call_next_diff_mode(context, mode):
from cleveragents.tui.permissions.models import DiffDisplayMode
from cleveragents.tui.permissions.screen import _next_diff_mode
context._next_mode_result = _next_diff_mode(DiffDisplayMode(mode))
@then('the next diff mode result should be "{mode}"')
def step_next_mode_result(context, mode):
assert context._next_mode_result == mode, (
f"Expected {mode!r}, got {context._next_mode_result!r}"
)
# ---------------------------------------------------------------------------
# Rendering helpers
# ---------------------------------------------------------------------------
@when("I render a file list with {n:d} files and selected index {idx:d}")
def step_render_file_list(context, n, idx):
from cleveragents.tui.permissions.models import FileChangeType, PermissionRequest
from cleveragents.tui.permissions.screen import _render_file_list
changes = [
PermissionRequest(
path=f"file{i}.py",
change_type=FileChangeType.MODIFIED,
before_content="old\n",
after_content="new\n",
)
for i in range(n)
]
context._file_list_text = _render_file_list(changes, idx)
_ARROW = "\u276f" # heavy right-pointing angle quotation mark ornament (U+276F)
@then(f'the file list should contain "{_ARROW}" on the second entry')
def step_file_list_arrow_second(context):
lines = context._file_list_text.splitlines()
# lines[0] is the header "Files (N changes):"
# lines[1] is file0 (index 0), lines[2] is file1 (index 1)
assert _ARROW in lines[2], f"Expected arrow in second file entry, got: {lines[2]!r}"
@when("I render a diff panel with no change")
def step_render_diff_panel_none(context):
from cleveragents.tui.permissions.models import DiffDisplayMode
from cleveragents.tui.permissions.screen import _render_diff_panel
context._diff_panel_text = _render_diff_panel(None, DiffDisplayMode.UNIFIED)
@when("I render a diff panel with an identical-content change")
def step_render_diff_panel_identical(context):
from cleveragents.tui.permissions.models import DiffDisplayMode
from cleveragents.tui.permissions.screen import _render_diff_panel
change = _make_permission_request("src/same.py", "same\n", "same\n")
context._diff_panel_text = _render_diff_panel(change, DiffDisplayMode.UNIFIED)
@then('the diff panel should contain "{text}"')
def step_diff_panel_contains(context, text):
assert text in context._diff_panel_text, (
f"Expected {text!r} in diff panel:\n{context._diff_panel_text}"
)
@when('I render a status bar for tool "{tool_name}" and resource "{resource_name}"')
def step_render_status_bar(context, tool_name, resource_name):
from cleveragents.tui.permissions.models import DiffDisplayMode
from cleveragents.tui.permissions.screen import _render_status_bar
context._status_bar_text = _render_status_bar(
tool_name, resource_name, DiffDisplayMode.UNIFIED
)
@then('the status bar should contain "{text}"')
def step_status_bar_contains(context, text):
assert text in context._status_bar_text, (
f"Expected {text!r} in status bar:\n{context._status_bar_text}"
)
# ---------------------------------------------------------------------------
# PermissionsScreen property accessors
# ---------------------------------------------------------------------------
@then("the screen current_request should be None")
def step_screen_current_request_none(context):
assert context._screen.current_request is None
@then("the screen current_request should not be None")
def step_screen_current_request_not_none(context):
assert context._screen.current_request is not None
# ---------------------------------------------------------------------------
# PermissionRequest split_diff delete/insert branches
# ---------------------------------------------------------------------------
@given("a PermissionRequest with only deleted lines")
def step_create_delete_only_request(context):
from cleveragents.tui.permissions.models import FileChangeType, PermissionRequest
context._perm_request = PermissionRequest(
path="src/deleted.py",
change_type=FileChangeType.MODIFIED,
before_content="line1\nline2\nline3\n",
after_content="",
)
@given("a PermissionRequest with only inserted lines")
def step_create_insert_only_request(context):
from cleveragents.tui.permissions.models import FileChangeType, PermissionRequest
context._perm_request = PermissionRequest(
path="src/inserted.py",
change_type=FileChangeType.MODIFIED,
before_content="",
after_content="line1\nline2\nline3\n",
)
@then("the left side should have non-empty lines")
def step_left_side_non_empty(context):
left, _ = context._perm_request.split_diff()
assert any(line.strip() for line in left), f"Expected non-empty left lines: {left}"
@then("the right side should have empty lines for deleted content")
def step_right_side_empty_for_deleted(context):
_, right = context._perm_request.split_diff()
# For delete-only, right side should have empty strings
assert any(line == "" for line in right), (
f"Expected empty right lines for deleted content: {right}"
)
@then("the right side should have non-empty lines")
def step_right_side_non_empty(context):
_, right = context._perm_request.split_diff()
assert any(line.strip() for line in right), (
f"Expected non-empty right lines: {right}"
)
@then("the left side should have empty lines for inserted content")
def step_left_side_empty_for_inserted(context):
left, _ = context._perm_request.split_diff()
# For insert-only, left side should have empty strings
assert any(line == "" for line in left), (
f"Expected empty left lines for inserted content: {left}"
)
# ---------------------------------------------------------------------------
# PermissionRequestService clear_session_decision return True
# ---------------------------------------------------------------------------
@then("the clear result should be True")
def step_clear_result_true(context):
assert context._clear_result is True
# ---------------------------------------------------------------------------
# Bug #10488: PermissionsScreen base class checks
# ---------------------------------------------------------------------------
@when("I check the base class of PermissionsScreen")
def step_check_base_class(context):
from cleveragents.tui.permissions.screen import PermissionsScreen
context._permissions_screen_cls = PermissionsScreen
@then("PermissionsScreen should be a subclass of textual.app.Screen")
def step_permissions_screen_is_screen_subclass(context):
import importlib
from cleveragents.tui.permissions import screen as screen_module
# Check the _ScreenBase variable in the module — if textual is available,
# it should be textual.app.Screen; if not, the fallback is used.
screen_base = getattr(screen_module, "_ScreenBase", None)
assert screen_base is not None, (
"Expected screen module to have _ScreenBase variable"
)
try:
Screen = importlib.import_module("textual.app").Screen
# Textual is available — verify PermissionsScreen inherits from Screen
assert issubclass(context._permissions_screen_cls, Screen), (
f"Expected PermissionsScreen to be a subclass of textual.app.Screen, "
f"but its MRO is: {[c.__name__ for c in context._permissions_screen_cls.__mro__]}"
)
assert screen_base is Screen, (
f"Expected _ScreenBase to be textual.app.Screen, got {screen_base!r}"
)
except ImportError:
# Textual not installed — verify the module is designed to use Screen
# by checking that _load_screen_base is defined (not _load_static_base).
load_fn = getattr(screen_module, "_load_screen_base", None)
assert load_fn is not None, (
"Expected screen module to have _load_screen_base function "
"(not _load_static_base). The module must be designed to load "
"textual.app.Screen as the base class."
)
@then("PermissionsScreen should have a BINDINGS class variable")
def step_permissions_screen_has_bindings(context):
cls = context._permissions_screen_cls
assert hasattr(cls, "BINDINGS"), (
"Expected PermissionsScreen to have a BINDINGS class variable"
)
assert cls.BINDINGS, "Expected PermissionsScreen.BINDINGS to be non-empty"
@then('PermissionsScreen should have action method "{method_name}"')
def step_permissions_screen_has_action_method(context, method_name):
cls = context._permissions_screen_cls
assert hasattr(cls, method_name), (
f"Expected PermissionsScreen to have action method '{method_name}'"
)
assert callable(getattr(cls, method_name)), (
f"Expected PermissionsScreen.{method_name} to be callable"
)