forked from HAL9000/cleveragents-core
0be3f85c56
Implement inline permission question widget for quick allow/reject decisions within the conversation stream with file diff context. ISSUES CLOSED: #997
305 lines
11 KiB
Python
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
|