Files
temp/features/steps/tui_permission_question_widget_steps.py
freemo 0be3f85c56 feat(tui): implement Permission Question Widget
Implement inline permission question widget for quick allow/reject
decisions within the conversation stream with file diff context.

ISSUES CLOSED: #997
2026-04-03 05:56:27 +00:00

305 lines
11 KiB
Python

"""Step definitions for tui_permission_question_widget.feature.
Tests for:
- InlinePermissionQuestion domain model
- PermissionQuestionWidget TUI widget
- render_permission_question helper
- Allow/reject actions
- Navigation (up/down/enter)
- Inline rendering in conversation stream
- Module exports
"""
from __future__ import annotations
import importlib
from unittest.mock import patch
from behave import given, then, when
import cleveragents.tui.widgets.permission_question as _pq_mod
from cleveragents.domain.models.core.inline_permission_question import (
InlinePermissionQuestion,
PermissionDecision,
PermissionRequestType,
)
from cleveragents.tui.widgets.permission_question import (
PermissionDecisionEvent,
)
# ---------------------------------------------------------------------------
# Background — force FallbackStatic so tests never need a live Textual app
# ---------------------------------------------------------------------------
@given("the permission question widget module is imported")
def step_module_imported(context: object) -> None:
"""Reload the module with textual patched out to force fallback path."""
with patch("importlib.import_module", side_effect=ImportError("no textual")):
importlib.reload(_pq_mod)
context._pq_widget_cls = _pq_mod.PermissionQuestionWidget # type: ignore[attr-defined]
context._pq_render_fn = _pq_mod.render_permission_question # type: ignore[attr-defined]
context._last_event: PermissionDecisionEvent | None = None # type: ignore[attr-defined]
def _restore() -> None:
importlib.reload(_pq_mod)
context.add_cleanup(_restore) # type: ignore[attr-defined]
# ---------------------------------------------------------------------------
# Domain model steps
# ---------------------------------------------------------------------------
@when('I create an InlinePermissionQuestion for "{file_path}" with type "{req_type}"')
def step_create_question(context: object, file_path: str, req_type: str) -> None:
context.question = InlinePermissionQuestion( # type: ignore[attr-defined]
file_path=file_path,
request_type=PermissionRequestType(req_type),
)
@given(
'an InlinePermissionQuestion for "{file_path}" with type "{req_type}"'
' and diff "{diff_content}"'
)
def step_create_question_with_diff(
context: object, file_path: str, req_type: str, diff_content: str
) -> None:
context.question = InlinePermissionQuestion( # type: ignore[attr-defined]
file_path=file_path,
request_type=PermissionRequestType(req_type),
diff_content=diff_content,
)
@then('the question file path should be "{expected}"')
def step_check_file_path(context: object, expected: str) -> None:
assert context.question.file_path == expected, ( # type: ignore[attr-defined]
f"Expected '{expected}', got '{context.question.file_path}'" # type: ignore[attr-defined]
)
@then('the question request type should be "{expected}"')
def step_check_request_type(context: object, expected: str) -> None:
assert context.question.request_type == PermissionRequestType(expected), ( # type: ignore[attr-defined]
f"Expected '{expected}', got '{context.question.request_type}'" # type: ignore[attr-defined]
)
@then("the question has_diff should be False")
def step_check_no_diff(context: object) -> None:
assert context.question.has_diff is False, "Expected has_diff to be False" # type: ignore[attr-defined]
@then("the question has_diff should be True")
def step_check_has_diff(context: object) -> None:
assert context.question.has_diff is True, "Expected has_diff to be True" # type: ignore[attr-defined]
@then('the description line should contain "{substring}"')
def step_check_description_line(context: object, substring: str) -> None:
line = context.question.description_line() # type: ignore[attr-defined]
assert substring in line, f"Expected '{substring}' in '{line}'"
# ---------------------------------------------------------------------------
# Widget creation steps
# ---------------------------------------------------------------------------
@when('I create a PermissionQuestionWidget for "{file_path}" with type "{req_type}"')
def step_create_widget(context: object, file_path: str, req_type: str) -> None:
question = InlinePermissionQuestion(
file_path=file_path,
request_type=PermissionRequestType(req_type),
)
context.widget = context._pq_widget_cls(question) # type: ignore[attr-defined]
context._last_event = None # type: ignore[attr-defined]
@given(
'a PermissionQuestionWidget for "{file_path}" with type "{req_type}"'
' and diff "{diff_content}"'
)
def step_create_widget_with_diff(
context: object, file_path: str, req_type: str, diff_content: str
) -> None:
question = InlinePermissionQuestion(
file_path=file_path,
request_type=PermissionRequestType(req_type),
diff_content=diff_content,
)
context.widget = context._pq_widget_cls(question) # type: ignore[attr-defined]
context._last_event = None # type: ignore[attr-defined]
@then('the widget text should contain "{substring}"')
def step_widget_text_contains(context: object, substring: str) -> None:
text = context.widget._text # type: ignore[attr-defined]
assert substring in text, f"Expected '{substring}' in widget text:\n{text}"
# ---------------------------------------------------------------------------
# Key press steps
# ---------------------------------------------------------------------------
@when('I press key "{key}" on the widget')
def step_press_key(context: object, key: str) -> None:
context._last_event = context.widget.handle_key(key) # type: ignore[attr-defined]
@then("a PermissionDecisionEvent should be returned")
def step_event_returned(context: object) -> None:
assert context._last_event is not None, (
"Expected a PermissionDecisionEvent but got None"
) # type: ignore[attr-defined]
# Use the PermissionDecisionEvent from the (possibly reloaded) module stored
# in context to avoid isinstance failures caused by module reload creating a
# new class object that differs from the top-level import.
pde_cls = _pq_mod.PermissionDecisionEvent
assert isinstance(context._last_event, pde_cls), ( # type: ignore[attr-defined]
f"Expected PermissionDecisionEvent, got {type(context._last_event)}" # type: ignore[attr-defined]
)
@then("no decision event should be returned")
def step_no_event_returned(context: object) -> None:
assert context._last_event is None, ( # type: ignore[attr-defined]
f"Expected None but got {context._last_event}" # type: ignore[attr-defined]
)
@then('the decision should be "{expected_decision}"')
def step_check_decision(context: object, expected_decision: str) -> None:
event: PermissionDecisionEvent = context._last_event # type: ignore[attr-defined]
assert event is not None, "No event was returned"
assert event.decision == PermissionDecision(expected_decision), (
f"Expected '{expected_decision}', got '{event.decision}'"
)
# ---------------------------------------------------------------------------
# Navigation steps
# ---------------------------------------------------------------------------
@then("the widget selected index should be {expected:d}")
def step_check_selected_index(context: object, expected: int) -> None:
idx = context.widget.selected_index # type: ignore[attr-defined]
assert idx == expected, f"Expected selected_index={expected}, got {idx}"
# ---------------------------------------------------------------------------
# open_full_screen flag
# ---------------------------------------------------------------------------
@then("the open_full_screen flag should be True")
def step_check_open_full_screen(context: object) -> None:
assert context.widget.open_full_screen is True, ( # type: ignore[attr-defined]
"Expected open_full_screen to be True"
)
# ---------------------------------------------------------------------------
# render_permission_question steps
# ---------------------------------------------------------------------------
@when('I render a permission question for "{file_path}" with type "{req_type}"')
def step_render_question(context: object, file_path: str, req_type: str) -> None:
question = InlinePermissionQuestion(
file_path=file_path,
request_type=PermissionRequestType(req_type),
)
context.rendered_text = context._pq_render_fn(question, 0) # type: ignore[attr-defined]
@when(
'I render a permission question for "{file_path}" with type "{req_type}"'
" at index {index:d}"
)
def step_render_question_at_index(
context: object, file_path: str, req_type: str, index: int
) -> None:
question = InlinePermissionQuestion(
file_path=file_path,
request_type=PermissionRequestType(req_type),
)
context.rendered_text = context._pq_render_fn(question, index) # type: ignore[attr-defined]
@given(
'a permission question for "{file_path}" with type "{req_type}"'
' and diff "{diff_content}" rendered at index {index:d} with show_diff'
)
def step_render_question_with_diff(
context: object,
file_path: str,
req_type: str,
diff_content: str,
index: int,
) -> None:
question = InlinePermissionQuestion(
file_path=file_path,
request_type=PermissionRequestType(req_type),
diff_content=diff_content,
)
context.rendered_text = context._pq_render_fn(question, index, show_diff=True) # type: ignore[attr-defined]
@then('the permission question rendered text should contain "{substring}"')
def step_pq_rendered_text_contains(context: object, substring: str) -> None:
text = context.rendered_text # type: ignore[attr-defined]
assert substring in text, f"Expected '{substring}' in rendered text:\n{text}"
# ---------------------------------------------------------------------------
# Export checks
# ---------------------------------------------------------------------------
@then("PermissionQuestionWidget should be importable from tui.widgets")
def step_check_widget_export(context: object) -> None:
from cleveragents.tui.widgets import PermissionQuestionWidget as _W
assert _W is not None
@then("PermissionDecisionEvent should be importable from tui.widgets")
def step_check_event_export(context: object) -> None:
from cleveragents.tui.widgets import PermissionDecisionEvent as _E
assert _E is not None
@then("render_permission_question should be importable from tui.widgets")
def step_check_render_export(context: object) -> None:
from cleveragents.tui.widgets import render_permission_question as _R
assert _R is not None
@then("InlinePermissionQuestion should be importable from domain.models.core")
def step_check_domain_export(context: object) -> None:
from cleveragents.domain.models.core import InlinePermissionQuestion as _M
assert _M is not None
@then("PermissionDecision should be importable from domain.models.core")
def step_check_decision_export(context: object) -> None:
from cleveragents.domain.models.core import PermissionDecision as _D
assert _D is not None
@then("PermissionRequestType should be importable from domain.models.core")
def step_check_request_type_export(context: object) -> None:
from cleveragents.domain.models.core import PermissionRequestType as _T
assert _T is not None