Files
cleveragents-core/features/tui_permissions_screen.feature
freemo 4a18a15ca5 feat(tui): implement PermissionsScreen with diff view
Implement PermissionsScreen with diff view for tool permission requests.
- 3 diff display modes (unified, side-by-side, context)
- Allow/reject keyboard bindings
- Permission decision persistence
- 62 BDD scenarios covering all components

ISSUES CLOSED: #996

Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Co-committed-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
2026-04-02 17:08:13 +00:00

376 lines
17 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", "side_by_side", "context"
# ── 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 side-by-side diff
Given a PermissionRequest for "src/auth/handler.py" with before and after content
When I render the diff in "side_by_side" mode
Then the rendered diff should contain "|"
Scenario: PermissionRequest generates a context diff
Given a PermissionRequest for "src/auth/handler.py" with before and after content
When I render the diff in "context" 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 side_by_side diff returns equal lines for identical content
Given a PermissionRequest for "src/empty.py" with identical before and after content
When I call side_by_side_diff on the request
Then both sides should have the same number of lines
Scenario: PermissionRequest context_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 "context" 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 context
Given a PermissionRequest for "src/auth/handler.py" with before and after content
When I render the diff in "context" 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 "side_by_side"
When I cycle the diff mode
Then the diff mode should be "context"
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 "side_by_side"
Then the diff mode should be "side_by_side"
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 side_by_side
When I call _next_diff_mode with "unified"
Then the next diff mode result should be "side_by_side"
Scenario: _next_diff_mode cycles side_by_side to context
When I call _next_diff_mode with "side_by_side"
Then the next diff mode result should be "context"
Scenario: _next_diff_mode cycles context back to unified
When I call _next_diff_mode with "context"
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 side_by_side_diff delete/insert branches ─
Scenario: PermissionRequest side_by_side_diff handles delete-only change
Given a PermissionRequest with only deleted lines
When I call side_by_side_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 side_by_side_diff handles insert-only change
Given a PermissionRequest with only inserted lines
When I call side_by_side_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