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
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
400 lines
18 KiB
Gherkin
400 lines
18 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", "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"
|