Files
cleveragents-core/features/tui_permissions_screen.feature
HAL9000 f86553670b
CI / lint (pull_request) Successful in 41s
CI / build (pull_request) Successful in 51s
CI / quality (pull_request) Successful in 55s
CI / helm (pull_request) Successful in 48s
CI / typecheck (pull_request) Successful in 1m15s
CI / security (pull_request) Successful in 1m19s
CI / push-validation (pull_request) Successful in 26s
CI / unit_tests (pull_request) Successful in 6m7s
CI / docker (pull_request) Successful in 1m45s
CI / integration_tests (pull_request) Successful in 11m3s
CI / coverage (pull_request) Successful in 11m59s
CI / status-check (pull_request) Successful in 4s
fix(tui): use correct DiffDisplayMode enum members + typed compose() result
The diff-mode cycle in PermissionsScreen referenced DiffDisplayMode.SIDE_BY_SIDE
and DiffDisplayMode.CONTEXT, but the enum only defines UNIFIED / SPLIT / AUTO.
Pyright flagged both as reportAttributeAccessIssue and behave failed to import
the screen module, masking the entire scenario suite under a single
traceback-outside-scenario error.

Also addresses the prior re-review feedback on the same PR:
- compose() return type was Any; tighten to collections.abc.Iterator[Any] so the
  generator shape is exposed to type checkers without taking a hard textual
  dependency at typecheck time (Iterator[Any] is the structural type of a
  Textual ComposeResult; we stay importable when textual is absent).
- The Bug #10488 TDD scenario asserting action methods now also covers
  action_dismiss_screen so a future refactor cannot silently drop the escape
  binding without test failure.

Verified locally on this worktree:
- typecheck gate: 0 errors, 4 unrelated warnings.
- unit_tests gate on features/tui_permissions_screen.feature: 65/65 scenarios
  pass.
- lint gate: clean.

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

400 lines
18 KiB
Gherkin
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Feature: TUI PermissionsScreen
Scenarios exercising the PermissionsScreen widget and supporting
domain models / service for tool permission requests.
# DiffDisplayMode enum
Scenario: DiffDisplayMode has three values
When I inspect the DiffDisplayMode enum
Then DiffDisplayMode should have values "unified", "split", "auto"
# ── FileChangeType enum ───────────────────────────────────────
Scenario: FileChangeType has three values
When I inspect the FileChangeType enum
Then FileChangeType should have values "M", "A", "D"
# ── PermissionDecision enum ───────────────────────────────────
Scenario: PermissionDecision has four values
When I inspect the PermissionDecision enum
Then PermissionDecision should have values "allow_once", "allow_always", "reject_once", "reject_always"
# ── PermissionRequest model ───────────────────────────────────
Scenario: PermissionRequest generates a unified diff
Given a PermissionRequest for "src/auth/handler.py" with before and after content
When I render the diff in "unified" mode
Then the rendered diff should contain "@@"
And the rendered diff should contain "+"
And the rendered diff should contain "-"
Scenario: PermissionRequest generates a split diff
Given a PermissionRequest for "src/auth/handler.py" with before and after content
When I render the diff in "split" mode
Then the rendered diff should contain "|"
Scenario: PermissionRequest generates an auto diff
Given a PermissionRequest for "src/auth/handler.py" with before and after content
When I render the diff in "auto" mode
Then the rendered diff should contain "***"
Scenario: PermissionRequest unified diff with no changes returns empty string
Given a PermissionRequest for "src/empty.py" with identical before and after content
When I render the diff in "unified" mode
Then the rendered diff should be empty
Scenario: PermissionRequest split diff returns equal lines for identical content
Given a PermissionRequest for "src/empty.py" with identical before and after content
When I call split_diff on the request
Then both sides should have the same number of lines
Scenario: PermissionRequest auto_diff with no changes returns empty string
Given a PermissionRequest for "src/empty.py" with identical before and after content
When I render the diff in "auto" mode
Then the rendered diff should be empty
Scenario: PermissionRequest render_diff dispatches to unified
Given a PermissionRequest for "src/auth/handler.py" with before and after content
When I render the diff in "unified" mode
Then the rendered diff should contain "a/src/auth/handler.py"
Scenario: PermissionRequest render_diff dispatches to auto
Given a PermissionRequest for "src/auth/handler.py" with before and after content
When I render the diff in "auto" mode
Then the rendered diff should contain "a/src/auth/handler.py"
# ── ToolPermissionRequest model ───────────────────────────────
Scenario: ToolPermissionRequest is pending when no decision is set
Given a ToolPermissionRequest with no decision
Then the request should be pending
And the request should not be allowed
And the request should not be rejected
Scenario: ToolPermissionRequest is allowed after allow_once decision
Given a ToolPermissionRequest with no decision
When I apply decision "allow_once" to the request
Then the request should be allowed
And the request should not be pending
Scenario: ToolPermissionRequest is allowed after allow_always decision
Given a ToolPermissionRequest with no decision
When I apply decision "allow_always" to the request
Then the request should be allowed
Scenario: ToolPermissionRequest is rejected after reject_once decision
Given a ToolPermissionRequest with no decision
When I apply decision "reject_once" to the request
Then the request should be rejected
And the request should not be pending
Scenario: ToolPermissionRequest is rejected after reject_always decision
Given a ToolPermissionRequest with no decision
When I apply decision "reject_always" to the request
Then the request should be rejected
Scenario: ToolPermissionRequest apply_decision returns a new instance
Given a ToolPermissionRequest with no decision
When I apply decision "allow_once" to the request
Then the original request should still be pending
# ── PermissionRequestService ──────────────────────────────────
Scenario: PermissionRequestService enqueues a request
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
Then the service should have 1 pending request
Scenario: PermissionRequestService records allow_once decision
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I record decision "allow_once" for request "req-1"
Then the service should have 0 pending requests
And the request "req-1" should have decision "allow_once"
Scenario: PermissionRequestService records reject_once decision
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I record decision "reject_once" for request "req-1"
Then the service should have 0 pending requests
And the request "req-1" should have decision "reject_once"
Scenario: PermissionRequestService auto-resolves with allow_always session decision
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I record decision "allow_always" for request "req-1"
And I enqueue a request with id "req-2" for tool "local/file-write"
Then the service should have 0 pending requests
And the request "req-2" should have decision "allow_always"
Scenario: PermissionRequestService auto-resolves with reject_always session decision
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I record decision "reject_always" for request "req-1"
And I enqueue a request with id "req-2" for tool "local/file-write"
Then the service should have 0 pending requests
And the request "req-2" should have decision "reject_always"
Scenario: PermissionRequestService get_session_decision returns None when not set
Given a fresh PermissionRequestService
Then the session decision for "local/file-write" should be None
Scenario: PermissionRequestService get_session_decision returns decision after allow_always
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I record decision "allow_always" for request "req-1"
Then the session decision for "local/file-write" should be "allow_always"
Scenario: PermissionRequestService clear_session_decision removes the decision
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I record decision "allow_always" for request "req-1"
And I clear the session decision for "local/file-write"
Then the session decision for "local/file-write" should be None
Scenario: PermissionRequestService clear_session_decision returns False when not set
Given a fresh PermissionRequestService
When I clear the session decision for "local/file-write"
Then the clear result should be False
Scenario: PermissionRequestService record_decision returns None for unknown id
Given a fresh PermissionRequestService
When I record decision "allow_once" for request "nonexistent"
Then the decision result should be None
Scenario: PermissionRequestService get returns None for unknown id
Given a fresh PermissionRequestService
Then getting request "nonexistent" should return None
Scenario: PermissionRequestService all_requests returns all requests
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I enqueue a request with id "req-2" for tool "local/file-write"
Then the service should have 2 total requests
Scenario: PermissionRequestService clear_all removes everything
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I record decision "allow_always" for request "req-1"
And I clear all requests
Then the service should have 0 total requests
And the session decision for "local/file-write" should be None
# ── PermissionsScreen widget ──────────────────────────────────
Scenario: PermissionsScreen loads a request and renders content
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
Then the screen text should contain "Permission Request"
And the screen text should contain "Files (3 changes)"
And the screen text should contain "local/file-write"
Scenario: PermissionsScreen shows the first file selected by default
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
Then the selected index should be 0
And the screen text should contain ""
Scenario: PermissionsScreen navigate_next moves to next file
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I navigate next on the screen
Then the selected index should be 1
Scenario: PermissionsScreen navigate_prev wraps around
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I navigate prev on the screen
Then the selected index should be 2
Scenario: PermissionsScreen navigate_next wraps around at end
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I navigate next on the screen
And I navigate next on the screen
And I navigate next on the screen
Then the selected index should be 0
Scenario: PermissionsScreen cycle_diff_mode cycles through modes
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
Then the diff mode should be "unified"
When I cycle the diff mode
Then the diff mode should be "split"
When I cycle the diff mode
Then the diff mode should be "auto"
When I cycle the diff mode
Then the diff mode should be "unified"
Scenario: PermissionsScreen set_diff_mode sets mode directly
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I set the diff mode to "split"
Then the diff mode should be "split"
Scenario: PermissionsScreen allow_once records decision
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I press allow_once on the screen
Then the screen decision should be "allow_once"
Scenario: PermissionsScreen allow_always records decision
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I press allow_always on the screen
Then the screen decision should be "allow_always"
Scenario: PermissionsScreen reject_once records decision
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I press reject_once on the screen
Then the screen decision should be "reject_once"
Scenario: PermissionsScreen reject_always records decision
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I press reject_always on the screen
Then the screen decision should be "reject_always"
Scenario: PermissionsScreen clear resets state
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
And I press allow_once on the screen
And I clear the screen
Then the screen decision should be None
And the screen text should be empty
Scenario: PermissionsScreen with no request shows placeholder
Given a fresh PermissionsScreen
Then the screen text should contain "(no permission request)"
Scenario: PermissionsScreen navigate_next does nothing with no request
Given a fresh PermissionsScreen
When I navigate next on the screen
Then the selected index should be 0
Scenario: PermissionsScreen navigate_prev does nothing with no request
Given a fresh PermissionsScreen
When I navigate prev on the screen
Then the selected index should be 0
Scenario: PermissionsScreen shows diff mode in status bar
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
Then the screen text should contain "unified"
Scenario: PermissionsScreen shows keyboard bindings in status bar
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
Then the screen text should contain "Allow"
And the screen text should contain "Reject"
Scenario: PermissionsScreen shows diff content for selected file
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
Then the screen text should contain "file0.py"
Scenario: PermissionsScreen navigate_next with empty changes list does nothing
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 0 file changes
And I navigate next on the screen
Then the selected index should be 0
Scenario: PermissionsScreen navigate_prev with empty changes list does nothing
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 0 file changes
And I navigate prev on the screen
Then the selected index should be 0
# ── _next_diff_mode helper ────────────────────────────────────
Scenario: _next_diff_mode cycles unified to split
When I call _next_diff_mode with "unified"
Then the next diff mode result should be "split"
Scenario: _next_diff_mode cycles split to auto
When I call _next_diff_mode with "split"
Then the next diff mode result should be "auto"
Scenario: _next_diff_mode cycles auto back to unified
When I call _next_diff_mode with "auto"
Then the next diff mode result should be "unified"
# ── Rendering helpers ─────────────────────────────────────────
Scenario: _render_file_list marks selected file with arrow
When I render a file list with 2 files and selected index 1
Then the file list should contain "" on the second entry
Scenario: _render_diff_panel returns placeholder for None change
When I render a diff panel with no change
Then the diff panel should contain "(no file selected)"
Scenario: _render_diff_panel returns placeholder for empty diff
When I render a diff panel with an identical-content change
Then the diff panel should contain "(no changes)"
Scenario: _render_status_bar includes tool name and resource name
When I render a status bar for tool "local/file-write" and resource "local/api-service"
Then the status bar should contain "local/file-write"
And the status bar should contain "local/api-service"
# ── PermissionsScreen property accessors ─────────────────────
Scenario: PermissionsScreen current_request returns None initially
Given a fresh PermissionsScreen
Then the screen current_request should be None
Scenario: PermissionsScreen current_request returns loaded request
Given a fresh PermissionsScreen
When I load a ToolPermissionRequest with 3 file changes
Then the screen current_request should not be None
# ── PermissionRequest split_diff delete/insert branches ────────
Scenario: PermissionRequest split_diff handles delete-only change
Given a PermissionRequest with only deleted lines
When I call split_diff on the request
Then the left side should have non-empty lines
And the right side should have empty lines for deleted content
Scenario: PermissionRequest split_diff handles insert-only change
Given a PermissionRequest with only inserted lines
When I call split_diff on the request
Then the right side should have non-empty lines
And the left side should have empty lines for inserted content
# ── PermissionRequestService clear_session_decision return True ─
Scenario: PermissionRequestService clear_session_decision returns True when decision exists
Given a fresh PermissionRequestService
When I enqueue a request with id "req-1" for tool "local/file-write"
And I record decision "allow_always" for request "req-1"
And I clear the session decision for "local/file-write"
Then the clear result should be True
# ── Bug #10488: PermissionsScreen must inherit from Screen ────
@tdd_issue @tdd_issue_10488
Scenario: Bug #10488 - PermissionsScreen inherits from textual.app.Screen
When I check the base class of PermissionsScreen
Then PermissionsScreen should be a subclass of textual.app.Screen
@tdd_issue @tdd_issue_10488
Scenario: Bug #10488 - PermissionsScreen has BINDINGS class variable
When I check the base class of PermissionsScreen
Then PermissionsScreen should have a BINDINGS class variable
@tdd_issue @tdd_issue_10488
Scenario: Bug #10488 - PermissionsScreen has action methods for keyboard bindings
When I check the base class of PermissionsScreen
Then PermissionsScreen should have action method "action_allow_once"
And PermissionsScreen should have action method "action_allow_always"
And PermissionsScreen should have action method "action_reject_once"
And PermissionsScreen should have action method "action_reject_always"
And PermissionsScreen should have action method "action_nav_next"
And PermissionsScreen should have action method "action_nav_prev"
And PermissionsScreen should have action method "action_cycle_diff"
And PermissionsScreen should have action method "action_dismiss_screen"