4a18a15ca5
CI / lint (push) Failing after 20s
CI / build (push) Successful in 19s
CI / quality (push) Has been cancelled
CI / helm (push) Successful in 22s
CI / typecheck (push) Has been cancelled
CI / e2e_tests (push) Has been cancelled
CI / integration_tests (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / security (push) Has been cancelled
CI / unit_tests (push) Has been cancelled
CI / status-check (push) Has been cancelled
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
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>
376 lines
17 KiB
Gherkin
376 lines
17 KiB
Gherkin
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
|