fix: harden command and API handling - #2

Merged
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings
Aug 4, 2026
Merged

fix: harden command and API handling#2
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings

Conversation

@solrevdev

@solrevdevsolrevdev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep credentials scoped to the Bitbucket API origin across redirects.
  • Stream downloads instead of buffering full responses in memory.
  • Handle nullable nested API data and encode endpoint path segments safely.
  • Save credentials atomically with strict Unix file permissions.
  • Return failures correctly from auth commands and centralise command error handling.
  • Propagate cancellation through command handlers.
  • Exit 130 and print nothing when a run is cancelled, and let a caller pass its own token to RunAsync.
  • Keep prompts on stderr so JSON and raw stdout remain safe for scripts.
  • Replace dynamic pipeline shaping with typed data and preserve the public output shape.
  • Add regression tests for the reviewed issues.

Cancellation, after review feedback

Copilot flagged InvokeAsync(..., default) in Program.cs, reading it as
Ctrl+C never reaching handlers. Ctrl+C did reach them: InvocationPipeline
links the token it is given to a source of its own and cancels it from
ProcessTerminationHandler, which registers for SIGINT and SIGTERM. Two real
problems sat next to it, both fixed in 9b855c9:

  • A caller driving RunAsync could not cancel, since the token was hardcoded.
  • Cancellation worked but did not exit cleanly. The OperationCanceledException
    fell to the catch-all and printed Error: The operation was canceled. with
    exit 1. It now exits 130 and prints nothing. An HttpClient timeout carries a
    TimeoutException inside, so it still reports as a failure with exit 1.

No Console.CancelKeyPress hook was added; it would only compete with the
library's own signal registration.

Validation

  • All 224 unit tests pass.
  • The project builds with zero warnings and zero errors.
  • The new cancellation test fails on the old code and passes on the new.
  • Ctrl+C sent through a real pty mid-request: old build exits 1 with an error
    line, new build exits 130 silently.
  • Read-only commands passed against existing Bitbucket repositories.
  • A full live write test passed in a throwaway Bitbucket repository, including
    upload, streamed download to a file and stdout, and SHA-256 checks.
  • A second throwaway repository covered repo create, src write,
    branch create, pr create, pr decline, branch delete and repo delete.
  • Both test artifacts and throwaway repositories were deleted, and repository
    absence was verified.

Safety

  • No existing Bitbucket repository was changed or deleted.
  • No SSH keys or security settings were changed.
  • The local credential file was not rewritten during live testing.

Keep authentication scoped to the Bitbucket API origin, handle nullable API data safely, stream downloads, preserve clean command output, and store credentials with safer file permissions. Add regression tests for each reviewed issue and propagate command cancellation throughout the CLI.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the CLI’s command boundary and Bitbucket API interactions by centralizing error handling, tightening credential behavior across redirects, improving streaming/encoding robustness, and adding regression tests to prevent regressions in script-safe output.

Changes:

  • Centralize command failure handling at the program boundary (exceptions + consistent non-zero exit codes) and propagate cancellation through handlers.
  • Harden API/IO behaviors: stream downloads, scope credentials to API origin across redirects, atomically persist credentials with strict file permissions, and safely escape endpoint path segments.
  • Improve resilience to nullable/nested API JSON (including pipelines output shaping) and add regression test coverage for the above.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/Bbx.Tests/ProgramTests.csAdds regression coverage for program-boundary error handling and exit codes.
tests/Bbx.Tests/Features/Users/UserHandlersTests.csUpdates user handler tests for revised auth/error behavior.
tests/Bbx.Tests/Features/Snippets/SnippetFilesHandlerTests.csVerifies per-segment path escaping for snippet file paths.
tests/Bbx.Tests/Features/Pipelines/PipelineFormatTests.csValidates pipeline formatting tolerates null nested members and preserves JSON shape.
tests/Bbx.Tests/Features/NullNestedObjectHandlerTests.csEnsures handlers tolerate null nested objects from the API.
tests/Bbx.Tests/Features/Downloads/GetDownloadHandlerTests.csUpdates download handler tests for streaming-to-destination behavior.
tests/Bbx.Tests/Features/Auth/AuthTokenHandlerTests.csUpdates auth token tests for exception-based user errors and return values.
tests/Bbx.Tests/Features/Auth/AuthStatusHandlerTests.csUpdates auth status tests for exception-based failures and API error propagation.
tests/Bbx.Tests/Commands/ConfirmationOutputTests.csEnsures destructive prompts write to stderr (stdout remains script-safe).
tests/Bbx.Tests/Commands/CommandRunnerTests.csUpdates expectations: errors now bubble to the program boundary.
tests/Bbx.Tests/Commands/CommandBindingTests.csVerifies System.CommandLine cancellation token reaches bound handlers.
tests/Bbx.Tests/Auth/FileCredentialStoreTests.csAdds tests for atomic credential writes and strict Unix permissions.
tests/Bbx.Tests/Api/BitbucketClientTests.csAdds redirect/auth-origin and streaming-copy coverage for the API client.
src/Bbx/Program.csCentralizes exception handling for the CLI and configures invocation behavior.
src/Bbx/Features/Workspaces/ViewWorkspace/ViewWorkspaceHandler.csRemoves redundant credential checks; hardens nested JSON handling via TryGetObject.
src/Bbx/Features/Workspaces/ListWorkspaces/ListWorkspacesHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Workspaces/ListWorkspacePermissions/ListWorkspacePermissionsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Workspaces/ListWorkspaceMembers/ListWorkspaceMembersHandler.csUses TryGetObject to avoid null-object pitfalls.
src/Bbx/Features/Users/ViewUser/ViewUserHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ViewSshKey/ViewSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ListSshKeys/ListSshKeysHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/DeleteSshKey/DeleteSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/AddSshKey/AddSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserWorkspacePermissions/ListUserWorkspacePermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserRepositoryPermissions/ListUserRepositoryPermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserEmails/ListUserEmailsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Source/LsSource/LsSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Source/CatSource/CatSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Snippets/ViewSnippet/ViewSnippetHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Snippets/UpdateSnippet/UpdateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetWatch/SnippetWatchHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetFiles/SnippetFilesHandler.csEscapes per-segment file paths while preserving directories.
src/Bbx/Features/Snippets/SnippetComments/SnippetCommentsHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/ListSnippets/ListSnippetsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Snippets/DeleteSnippet/DeleteSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/CreateSnippet/CreateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Repos/RepoPermissions/RepoPermissionsHandler.csAvoids null nested-object access when shaping permissions output.
src/Bbx/Features/Pipelines/ViewPipeline/ViewPipelineHandler.csHardens steps shaping against non-array/null values.
src/Bbx/Features/Pipelines/ViewDeploymentEnvironment/ViewDeploymentEnvironmentHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/PipelineFormat.csReplaces dynamic shaping with typed records while preserving output shape.
src/Bbx/Features/Pipelines/ListPipelineVariables/ListPipelineVariablesHandler.csSafely reads boolean properties without throwing on null/non-bool.
src/Bbx/Features/Pipelines/ListPipelineSchedules/ListPipelineSchedulesHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/ListDeploymentEnvironments/ListDeploymentEnvironmentsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Downloads/GetDownload/GetDownloadHandler.csSwitches download handling to streaming via a destination stream.
src/Bbx/Features/Common/EndpointPath.csIntroduces shared helper for per-segment path escaping.
src/Bbx/Features/Commits/FileHistory/FileHistoryHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Auth/Token/AuthTokenHandler.csReturns token string and throws user errors instead of writing/setting exit code.
src/Bbx/Features/Auth/Status/AuthStatusHandler.csReturns status string and lets API failures propagate to the program boundary.
src/Bbx/Commands/WorkspaceCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/UserCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SrcCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SnippetCommand.csCentralizes direct command error handling and propagates cancellation.
src/Bbx/Commands/RepoCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/PrCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/PipelineCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/IssueCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/DownloadCommand.csStreams downloads to file/stdout with safer temp-file handling and cancellation support.
src/Bbx/Commands/CommitCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/CommandRunner.csRefactors runner helpers to rely on program-boundary exception handling and adds stream runner.
src/Bbx/Commands/CommandBinding.csAdds AsyncLocal cancellation token propagation into handlers.
src/Bbx/Commands/BranchCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/AuthCommand.csSwitches auth commands to throw user errors and standardizes cancellation propagation.
src/Bbx/Auth/FileCredentialStore.csMakes credential load failures explicit; saves atomically with strict Unix permissions.
src/Bbx/Api/BitbucketClient.csAdds streaming CopyToAsync and tightens auth scoping across redirects/origins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Bbx/Program.cs Outdated
Comment on lines +69 to +72
exitCode = await parseResult.InvokeAsync(new InvocationConfiguration
{
EnableDefaultExceptionHandler = false,
}, default);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half right, so I checked it against the library rather than the description. Fixed in 9b855c9.

Ctrl+C already reached handlers.InvocationPipeline.InvokeAsync does CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) and gives the action cts.Token, then hands that same source to ProcessTerminationHandler, which registers for SIGINT and SIGTERM via PosixSignalRegistration on .NET 7+. So the token CommandBinding sees is cancelable even when default goes in, and a signal cancels it.

Verified against the built tool, Ctrl+C sent through a real pty mid-request:

baseline, no Ctrl+C: exit=0, 4988 bytes of JSON
Ctrl+C at 0.4s: exit=130, no output

I did not take the Console.CancelKeyPress suggestion. ProcessTerminationHandler only falls back to CancelKeyPress on platforms without PosixSignalRegistration; adding our own subscriber would be a second handler competing with the library's for the same signal.

Two things were wrong. First, a caller driving RunAsync had no way to cancel, since the token was hardcoded. It now takes an optional CancellationToken.

Second, and more visible: cancellation worked but did not exit cleanly. The OperationCanceledException fell through to the catch-all and printed Error: The operation was canceled. with exit 1. Confirmed on the pre-change build:

OLD build, Ctrl+C at 0.4s: exit=1, "Error: The operation was canceled."
NEW build, Ctrl+C at 0.4s: exit=130, no output

A cancelled run is not an error, so it now exits 130, the shell convention for SIGINT, and prints nothing. An HttpClient timeout also surfaces as an OperationCanceledException, but carries a TimeoutException inside, so the filter lets it through to the failure path and it still exits 1.

Tests.Cancelling_the_supplied_token_stops_the_command_before_it_calls_the_api fails on the old code with Expected exitCode to be 130, but found 0 and passes on the new. Bound_handlers_get_a_cancelable_token_even_with_no_token_supplied pins the library behaviour above, so anyone reading this thread and reaching for a signal hook gets a test explaining why not.

That test needed one test-kit fix: FakeHttpMessageHandler ignored the token. HttpClient passes the token straight to its handler without checking it first, so the fake was answering requests a real handler would have refused.

224 tests pass. Also exercised end to end with the packed tool: read-only commands against real repos, and the write and destructive paths (repo create, src write, branch create, pr create, pr decline, branch delete, repo delete) against a throwaway repo, since deleted.

Program.RunAsync hardcoded `default` as the token for InvokeAsync, so a
caller driving RunAsync had no way to stop a command. It now takes an
optional token and passes it through.
Ctrl+C already reached handlers: System.CommandLine links whatever token
it is given to a source of its own and cancels that from
ProcessTerminationHandler. What it did not do was exit cleanly. The
resulting OperationCanceledException fell to the catch-all and printed
"Error: The operation was canceled." with exit 1. A cancelled run is not
an error, so it now exits 130 and says nothing. An HttpClient timeout
also surfaces as an OperationCanceledException but carries a
TimeoutException inside, so it still reports as a failure with exit 1.
FakeHttpMessageHandler now honours the token. HttpClient hands it to the
handler without checking it first, so a fake that ignored it answered
requests a real handler would have refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.

@solrevdev
solrevdev merged commit 4706ca7 into masterAug 4, 2026
3 checks passed
@solrevdev
solrevdev deleted the fix/repository-review-findings branch August 4, 2026 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@solrevdev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: harden command and API handling - #2

Merged
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings
Aug 4, 2026
Merged

fix: harden command and API handling#2
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings

Conversation

@solrevdev

@solrevdevsolrevdev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep credentials scoped to the Bitbucket API origin across redirects.
  • Stream downloads instead of buffering full responses in memory.
  • Handle nullable nested API data and encode endpoint path segments safely.
  • Save credentials atomically with strict Unix file permissions.
  • Return failures correctly from auth commands and centralise command error handling.
  • Propagate cancellation through command handlers.
  • Exit 130 and print nothing when a run is cancelled, and let a caller pass its own token to RunAsync.
  • Keep prompts on stderr so JSON and raw stdout remain safe for scripts.
  • Replace dynamic pipeline shaping with typed data and preserve the public output shape.
  • Add regression tests for the reviewed issues.

Cancellation, after review feedback

Copilot flagged InvokeAsync(..., default) in Program.cs, reading it as
Ctrl+C never reaching handlers. Ctrl+C did reach them: InvocationPipeline
links the token it is given to a source of its own and cancels it from
ProcessTerminationHandler, which registers for SIGINT and SIGTERM. Two real
problems sat next to it, both fixed in 9b855c9:

  • A caller driving RunAsync could not cancel, since the token was hardcoded.
  • Cancellation worked but did not exit cleanly. The OperationCanceledException
    fell to the catch-all and printed Error: The operation was canceled. with
    exit 1. It now exits 130 and prints nothing. An HttpClient timeout carries a
    TimeoutException inside, so it still reports as a failure with exit 1.

No Console.CancelKeyPress hook was added; it would only compete with the
library's own signal registration.

Validation

  • All 224 unit tests pass.
  • The project builds with zero warnings and zero errors.
  • The new cancellation test fails on the old code and passes on the new.
  • Ctrl+C sent through a real pty mid-request: old build exits 1 with an error
    line, new build exits 130 silently.
  • Read-only commands passed against existing Bitbucket repositories.
  • A full live write test passed in a throwaway Bitbucket repository, including
    upload, streamed download to a file and stdout, and SHA-256 checks.
  • A second throwaway repository covered repo create, src write,
    branch create, pr create, pr decline, branch delete and repo delete.
  • Both test artifacts and throwaway repositories were deleted, and repository
    absence was verified.

Safety

  • No existing Bitbucket repository was changed or deleted.
  • No SSH keys or security settings were changed.
  • The local credential file was not rewritten during live testing.

Keep authentication scoped to the Bitbucket API origin, handle nullable API data safely, stream downloads, preserve clean command output, and store credentials with safer file permissions. Add regression tests for each reviewed issue and propagate command cancellation throughout the CLI.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the CLI’s command boundary and Bitbucket API interactions by centralizing error handling, tightening credential behavior across redirects, improving streaming/encoding robustness, and adding regression tests to prevent regressions in script-safe output.

Changes:

  • Centralize command failure handling at the program boundary (exceptions + consistent non-zero exit codes) and propagate cancellation through handlers.
  • Harden API/IO behaviors: stream downloads, scope credentials to API origin across redirects, atomically persist credentials with strict file permissions, and safely escape endpoint path segments.
  • Improve resilience to nullable/nested API JSON (including pipelines output shaping) and add regression test coverage for the above.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/Bbx.Tests/ProgramTests.csAdds regression coverage for program-boundary error handling and exit codes.
tests/Bbx.Tests/Features/Users/UserHandlersTests.csUpdates user handler tests for revised auth/error behavior.
tests/Bbx.Tests/Features/Snippets/SnippetFilesHandlerTests.csVerifies per-segment path escaping for snippet file paths.
tests/Bbx.Tests/Features/Pipelines/PipelineFormatTests.csValidates pipeline formatting tolerates null nested members and preserves JSON shape.
tests/Bbx.Tests/Features/NullNestedObjectHandlerTests.csEnsures handlers tolerate null nested objects from the API.
tests/Bbx.Tests/Features/Downloads/GetDownloadHandlerTests.csUpdates download handler tests for streaming-to-destination behavior.
tests/Bbx.Tests/Features/Auth/AuthTokenHandlerTests.csUpdates auth token tests for exception-based user errors and return values.
tests/Bbx.Tests/Features/Auth/AuthStatusHandlerTests.csUpdates auth status tests for exception-based failures and API error propagation.
tests/Bbx.Tests/Commands/ConfirmationOutputTests.csEnsures destructive prompts write to stderr (stdout remains script-safe).
tests/Bbx.Tests/Commands/CommandRunnerTests.csUpdates expectations: errors now bubble to the program boundary.
tests/Bbx.Tests/Commands/CommandBindingTests.csVerifies System.CommandLine cancellation token reaches bound handlers.
tests/Bbx.Tests/Auth/FileCredentialStoreTests.csAdds tests for atomic credential writes and strict Unix permissions.
tests/Bbx.Tests/Api/BitbucketClientTests.csAdds redirect/auth-origin and streaming-copy coverage for the API client.
src/Bbx/Program.csCentralizes exception handling for the CLI and configures invocation behavior.
src/Bbx/Features/Workspaces/ViewWorkspace/ViewWorkspaceHandler.csRemoves redundant credential checks; hardens nested JSON handling via TryGetObject.
src/Bbx/Features/Workspaces/ListWorkspaces/ListWorkspacesHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Workspaces/ListWorkspacePermissions/ListWorkspacePermissionsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Workspaces/ListWorkspaceMembers/ListWorkspaceMembersHandler.csUses TryGetObject to avoid null-object pitfalls.
src/Bbx/Features/Users/ViewUser/ViewUserHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ViewSshKey/ViewSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ListSshKeys/ListSshKeysHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/DeleteSshKey/DeleteSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/AddSshKey/AddSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserWorkspacePermissions/ListUserWorkspacePermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserRepositoryPermissions/ListUserRepositoryPermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserEmails/ListUserEmailsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Source/LsSource/LsSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Source/CatSource/CatSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Snippets/ViewSnippet/ViewSnippetHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Snippets/UpdateSnippet/UpdateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetWatch/SnippetWatchHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetFiles/SnippetFilesHandler.csEscapes per-segment file paths while preserving directories.
src/Bbx/Features/Snippets/SnippetComments/SnippetCommentsHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/ListSnippets/ListSnippetsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Snippets/DeleteSnippet/DeleteSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/CreateSnippet/CreateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Repos/RepoPermissions/RepoPermissionsHandler.csAvoids null nested-object access when shaping permissions output.
src/Bbx/Features/Pipelines/ViewPipeline/ViewPipelineHandler.csHardens steps shaping against non-array/null values.
src/Bbx/Features/Pipelines/ViewDeploymentEnvironment/ViewDeploymentEnvironmentHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/PipelineFormat.csReplaces dynamic shaping with typed records while preserving output shape.
src/Bbx/Features/Pipelines/ListPipelineVariables/ListPipelineVariablesHandler.csSafely reads boolean properties without throwing on null/non-bool.
src/Bbx/Features/Pipelines/ListPipelineSchedules/ListPipelineSchedulesHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/ListDeploymentEnvironments/ListDeploymentEnvironmentsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Downloads/GetDownload/GetDownloadHandler.csSwitches download handling to streaming via a destination stream.
src/Bbx/Features/Common/EndpointPath.csIntroduces shared helper for per-segment path escaping.
src/Bbx/Features/Commits/FileHistory/FileHistoryHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Auth/Token/AuthTokenHandler.csReturns token string and throws user errors instead of writing/setting exit code.
src/Bbx/Features/Auth/Status/AuthStatusHandler.csReturns status string and lets API failures propagate to the program boundary.
src/Bbx/Commands/WorkspaceCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/UserCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SrcCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SnippetCommand.csCentralizes direct command error handling and propagates cancellation.
src/Bbx/Commands/RepoCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/PrCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/PipelineCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/IssueCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/DownloadCommand.csStreams downloads to file/stdout with safer temp-file handling and cancellation support.
src/Bbx/Commands/CommitCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/CommandRunner.csRefactors runner helpers to rely on program-boundary exception handling and adds stream runner.
src/Bbx/Commands/CommandBinding.csAdds AsyncLocal cancellation token propagation into handlers.
src/Bbx/Commands/BranchCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/AuthCommand.csSwitches auth commands to throw user errors and standardizes cancellation propagation.
src/Bbx/Auth/FileCredentialStore.csMakes credential load failures explicit; saves atomically with strict Unix permissions.
src/Bbx/Api/BitbucketClient.csAdds streaming CopyToAsync and tightens auth scoping across redirects/origins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Bbx/Program.cs Outdated
Comment on lines +69 to +72
exitCode = await parseResult.InvokeAsync(new InvocationConfiguration
{
EnableDefaultExceptionHandler = false,
}, default);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half right, so I checked it against the library rather than the description. Fixed in 9b855c9.

Ctrl+C already reached handlers.InvocationPipeline.InvokeAsync does CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) and gives the action cts.Token, then hands that same source to ProcessTerminationHandler, which registers for SIGINT and SIGTERM via PosixSignalRegistration on .NET 7+. So the token CommandBinding sees is cancelable even when default goes in, and a signal cancels it.

Verified against the built tool, Ctrl+C sent through a real pty mid-request:

baseline, no Ctrl+C: exit=0, 4988 bytes of JSON
Ctrl+C at 0.4s: exit=130, no output

I did not take the Console.CancelKeyPress suggestion. ProcessTerminationHandler only falls back to CancelKeyPress on platforms without PosixSignalRegistration; adding our own subscriber would be a second handler competing with the library's for the same signal.

Two things were wrong. First, a caller driving RunAsync had no way to cancel, since the token was hardcoded. It now takes an optional CancellationToken.

Second, and more visible: cancellation worked but did not exit cleanly. The OperationCanceledException fell through to the catch-all and printed Error: The operation was canceled. with exit 1. Confirmed on the pre-change build:

OLD build, Ctrl+C at 0.4s: exit=1, "Error: The operation was canceled."
NEW build, Ctrl+C at 0.4s: exit=130, no output

A cancelled run is not an error, so it now exits 130, the shell convention for SIGINT, and prints nothing. An HttpClient timeout also surfaces as an OperationCanceledException, but carries a TimeoutException inside, so the filter lets it through to the failure path and it still exits 1.

Tests.Cancelling_the_supplied_token_stops_the_command_before_it_calls_the_api fails on the old code with Expected exitCode to be 130, but found 0 and passes on the new. Bound_handlers_get_a_cancelable_token_even_with_no_token_supplied pins the library behaviour above, so anyone reading this thread and reaching for a signal hook gets a test explaining why not.

That test needed one test-kit fix: FakeHttpMessageHandler ignored the token. HttpClient passes the token straight to its handler without checking it first, so the fake was answering requests a real handler would have refused.

224 tests pass. Also exercised end to end with the packed tool: read-only commands against real repos, and the write and destructive paths (repo create, src write, branch create, pr create, pr decline, branch delete, repo delete) against a throwaway repo, since deleted.

Program.RunAsync hardcoded `default` as the token for InvokeAsync, so a
caller driving RunAsync had no way to stop a command. It now takes an
optional token and passes it through.
Ctrl+C already reached handlers: System.CommandLine links whatever token
it is given to a source of its own and cancels that from
ProcessTerminationHandler. What it did not do was exit cleanly. The
resulting OperationCanceledException fell to the catch-all and printed
"Error: The operation was canceled." with exit 1. A cancelled run is not
an error, so it now exits 130 and says nothing. An HttpClient timeout
also surfaces as an OperationCanceledException but carries a
TimeoutException inside, so it still reports as a failure with exit 1.
FakeHttpMessageHandler now honours the token. HttpClient hands it to the
handler without checking it first, so a fake that ignored it answered
requests a real handler would have refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.

@solrevdev
solrevdev merged commit 4706ca7 into masterAug 4, 2026
3 checks passed
@solrevdev
solrevdev deleted the fix/repository-review-findings branch August 4, 2026 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@solrevdev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: harden command and API handling - #2

Merged
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings
Aug 4, 2026
Merged

fix: harden command and API handling#2
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings

Conversation

@solrevdev

@solrevdevsolrevdev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep credentials scoped to the Bitbucket API origin across redirects.
  • Stream downloads instead of buffering full responses in memory.
  • Handle nullable nested API data and encode endpoint path segments safely.
  • Save credentials atomically with strict Unix file permissions.
  • Return failures correctly from auth commands and centralise command error handling.
  • Propagate cancellation through command handlers.
  • Exit 130 and print nothing when a run is cancelled, and let a caller pass its own token to RunAsync.
  • Keep prompts on stderr so JSON and raw stdout remain safe for scripts.
  • Replace dynamic pipeline shaping with typed data and preserve the public output shape.
  • Add regression tests for the reviewed issues.

Cancellation, after review feedback

Copilot flagged InvokeAsync(..., default) in Program.cs, reading it as
Ctrl+C never reaching handlers. Ctrl+C did reach them: InvocationPipeline
links the token it is given to a source of its own and cancels it from
ProcessTerminationHandler, which registers for SIGINT and SIGTERM. Two real
problems sat next to it, both fixed in 9b855c9:

  • A caller driving RunAsync could not cancel, since the token was hardcoded.
  • Cancellation worked but did not exit cleanly. The OperationCanceledException
    fell to the catch-all and printed Error: The operation was canceled. with
    exit 1. It now exits 130 and prints nothing. An HttpClient timeout carries a
    TimeoutException inside, so it still reports as a failure with exit 1.

No Console.CancelKeyPress hook was added; it would only compete with the
library's own signal registration.

Validation

  • All 224 unit tests pass.
  • The project builds with zero warnings and zero errors.
  • The new cancellation test fails on the old code and passes on the new.
  • Ctrl+C sent through a real pty mid-request: old build exits 1 with an error
    line, new build exits 130 silently.
  • Read-only commands passed against existing Bitbucket repositories.
  • A full live write test passed in a throwaway Bitbucket repository, including
    upload, streamed download to a file and stdout, and SHA-256 checks.
  • A second throwaway repository covered repo create, src write,
    branch create, pr create, pr decline, branch delete and repo delete.
  • Both test artifacts and throwaway repositories were deleted, and repository
    absence was verified.

Safety

  • No existing Bitbucket repository was changed or deleted.
  • No SSH keys or security settings were changed.
  • The local credential file was not rewritten during live testing.

Keep authentication scoped to the Bitbucket API origin, handle nullable API data safely, stream downloads, preserve clean command output, and store credentials with safer file permissions. Add regression tests for each reviewed issue and propagate command cancellation throughout the CLI.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the CLI’s command boundary and Bitbucket API interactions by centralizing error handling, tightening credential behavior across redirects, improving streaming/encoding robustness, and adding regression tests to prevent regressions in script-safe output.

Changes:

  • Centralize command failure handling at the program boundary (exceptions + consistent non-zero exit codes) and propagate cancellation through handlers.
  • Harden API/IO behaviors: stream downloads, scope credentials to API origin across redirects, atomically persist credentials with strict file permissions, and safely escape endpoint path segments.
  • Improve resilience to nullable/nested API JSON (including pipelines output shaping) and add regression test coverage for the above.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/Bbx.Tests/ProgramTests.csAdds regression coverage for program-boundary error handling and exit codes.
tests/Bbx.Tests/Features/Users/UserHandlersTests.csUpdates user handler tests for revised auth/error behavior.
tests/Bbx.Tests/Features/Snippets/SnippetFilesHandlerTests.csVerifies per-segment path escaping for snippet file paths.
tests/Bbx.Tests/Features/Pipelines/PipelineFormatTests.csValidates pipeline formatting tolerates null nested members and preserves JSON shape.
tests/Bbx.Tests/Features/NullNestedObjectHandlerTests.csEnsures handlers tolerate null nested objects from the API.
tests/Bbx.Tests/Features/Downloads/GetDownloadHandlerTests.csUpdates download handler tests for streaming-to-destination behavior.
tests/Bbx.Tests/Features/Auth/AuthTokenHandlerTests.csUpdates auth token tests for exception-based user errors and return values.
tests/Bbx.Tests/Features/Auth/AuthStatusHandlerTests.csUpdates auth status tests for exception-based failures and API error propagation.
tests/Bbx.Tests/Commands/ConfirmationOutputTests.csEnsures destructive prompts write to stderr (stdout remains script-safe).
tests/Bbx.Tests/Commands/CommandRunnerTests.csUpdates expectations: errors now bubble to the program boundary.
tests/Bbx.Tests/Commands/CommandBindingTests.csVerifies System.CommandLine cancellation token reaches bound handlers.
tests/Bbx.Tests/Auth/FileCredentialStoreTests.csAdds tests for atomic credential writes and strict Unix permissions.
tests/Bbx.Tests/Api/BitbucketClientTests.csAdds redirect/auth-origin and streaming-copy coverage for the API client.
src/Bbx/Program.csCentralizes exception handling for the CLI and configures invocation behavior.
src/Bbx/Features/Workspaces/ViewWorkspace/ViewWorkspaceHandler.csRemoves redundant credential checks; hardens nested JSON handling via TryGetObject.
src/Bbx/Features/Workspaces/ListWorkspaces/ListWorkspacesHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Workspaces/ListWorkspacePermissions/ListWorkspacePermissionsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Workspaces/ListWorkspaceMembers/ListWorkspaceMembersHandler.csUses TryGetObject to avoid null-object pitfalls.
src/Bbx/Features/Users/ViewUser/ViewUserHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ViewSshKey/ViewSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ListSshKeys/ListSshKeysHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/DeleteSshKey/DeleteSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/AddSshKey/AddSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserWorkspacePermissions/ListUserWorkspacePermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserRepositoryPermissions/ListUserRepositoryPermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserEmails/ListUserEmailsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Source/LsSource/LsSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Source/CatSource/CatSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Snippets/ViewSnippet/ViewSnippetHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Snippets/UpdateSnippet/UpdateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetWatch/SnippetWatchHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetFiles/SnippetFilesHandler.csEscapes per-segment file paths while preserving directories.
src/Bbx/Features/Snippets/SnippetComments/SnippetCommentsHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/ListSnippets/ListSnippetsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Snippets/DeleteSnippet/DeleteSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/CreateSnippet/CreateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Repos/RepoPermissions/RepoPermissionsHandler.csAvoids null nested-object access when shaping permissions output.
src/Bbx/Features/Pipelines/ViewPipeline/ViewPipelineHandler.csHardens steps shaping against non-array/null values.
src/Bbx/Features/Pipelines/ViewDeploymentEnvironment/ViewDeploymentEnvironmentHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/PipelineFormat.csReplaces dynamic shaping with typed records while preserving output shape.
src/Bbx/Features/Pipelines/ListPipelineVariables/ListPipelineVariablesHandler.csSafely reads boolean properties without throwing on null/non-bool.
src/Bbx/Features/Pipelines/ListPipelineSchedules/ListPipelineSchedulesHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/ListDeploymentEnvironments/ListDeploymentEnvironmentsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Downloads/GetDownload/GetDownloadHandler.csSwitches download handling to streaming via a destination stream.
src/Bbx/Features/Common/EndpointPath.csIntroduces shared helper for per-segment path escaping.
src/Bbx/Features/Commits/FileHistory/FileHistoryHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Auth/Token/AuthTokenHandler.csReturns token string and throws user errors instead of writing/setting exit code.
src/Bbx/Features/Auth/Status/AuthStatusHandler.csReturns status string and lets API failures propagate to the program boundary.
src/Bbx/Commands/WorkspaceCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/UserCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SrcCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SnippetCommand.csCentralizes direct command error handling and propagates cancellation.
src/Bbx/Commands/RepoCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/PrCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/PipelineCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/IssueCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/DownloadCommand.csStreams downloads to file/stdout with safer temp-file handling and cancellation support.
src/Bbx/Commands/CommitCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/CommandRunner.csRefactors runner helpers to rely on program-boundary exception handling and adds stream runner.
src/Bbx/Commands/CommandBinding.csAdds AsyncLocal cancellation token propagation into handlers.
src/Bbx/Commands/BranchCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/AuthCommand.csSwitches auth commands to throw user errors and standardizes cancellation propagation.
src/Bbx/Auth/FileCredentialStore.csMakes credential load failures explicit; saves atomically with strict Unix permissions.
src/Bbx/Api/BitbucketClient.csAdds streaming CopyToAsync and tightens auth scoping across redirects/origins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Bbx/Program.cs Outdated
Comment on lines +69 to +72
exitCode = await parseResult.InvokeAsync(new InvocationConfiguration
{
EnableDefaultExceptionHandler = false,
}, default);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half right, so I checked it against the library rather than the description. Fixed in 9b855c9.

Ctrl+C already reached handlers.InvocationPipeline.InvokeAsync does CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) and gives the action cts.Token, then hands that same source to ProcessTerminationHandler, which registers for SIGINT and SIGTERM via PosixSignalRegistration on .NET 7+. So the token CommandBinding sees is cancelable even when default goes in, and a signal cancels it.

Verified against the built tool, Ctrl+C sent through a real pty mid-request:

baseline, no Ctrl+C: exit=0, 4988 bytes of JSON
Ctrl+C at 0.4s: exit=130, no output

I did not take the Console.CancelKeyPress suggestion. ProcessTerminationHandler only falls back to CancelKeyPress on platforms without PosixSignalRegistration; adding our own subscriber would be a second handler competing with the library's for the same signal.

Two things were wrong. First, a caller driving RunAsync had no way to cancel, since the token was hardcoded. It now takes an optional CancellationToken.

Second, and more visible: cancellation worked but did not exit cleanly. The OperationCanceledException fell through to the catch-all and printed Error: The operation was canceled. with exit 1. Confirmed on the pre-change build:

OLD build, Ctrl+C at 0.4s: exit=1, "Error: The operation was canceled."
NEW build, Ctrl+C at 0.4s: exit=130, no output

A cancelled run is not an error, so it now exits 130, the shell convention for SIGINT, and prints nothing. An HttpClient timeout also surfaces as an OperationCanceledException, but carries a TimeoutException inside, so the filter lets it through to the failure path and it still exits 1.

Tests.Cancelling_the_supplied_token_stops_the_command_before_it_calls_the_api fails on the old code with Expected exitCode to be 130, but found 0 and passes on the new. Bound_handlers_get_a_cancelable_token_even_with_no_token_supplied pins the library behaviour above, so anyone reading this thread and reaching for a signal hook gets a test explaining why not.

That test needed one test-kit fix: FakeHttpMessageHandler ignored the token. HttpClient passes the token straight to its handler without checking it first, so the fake was answering requests a real handler would have refused.

224 tests pass. Also exercised end to end with the packed tool: read-only commands against real repos, and the write and destructive paths (repo create, src write, branch create, pr create, pr decline, branch delete, repo delete) against a throwaway repo, since deleted.

Program.RunAsync hardcoded `default` as the token for InvokeAsync, so a
caller driving RunAsync had no way to stop a command. It now takes an
optional token and passes it through.
Ctrl+C already reached handlers: System.CommandLine links whatever token
it is given to a source of its own and cancels that from
ProcessTerminationHandler. What it did not do was exit cleanly. The
resulting OperationCanceledException fell to the catch-all and printed
"Error: The operation was canceled." with exit 1. A cancelled run is not
an error, so it now exits 130 and says nothing. An HttpClient timeout
also surfaces as an OperationCanceledException but carries a
TimeoutException inside, so it still reports as a failure with exit 1.
FakeHttpMessageHandler now honours the token. HttpClient hands it to the
handler without checking it first, so a fake that ignored it answered
requests a real handler would have refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.

@solrevdev
solrevdev merged commit 4706ca7 into masterAug 4, 2026
3 checks passed
@solrevdev
solrevdev deleted the fix/repository-review-findings branch August 4, 2026 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@solrevdev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: harden command and API handling - #2

Merged
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings
Aug 4, 2026
Merged

fix: harden command and API handling#2
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings

Conversation

@solrevdev

@solrevdevsolrevdev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep credentials scoped to the Bitbucket API origin across redirects.
  • Stream downloads instead of buffering full responses in memory.
  • Handle nullable nested API data and encode endpoint path segments safely.
  • Save credentials atomically with strict Unix file permissions.
  • Return failures correctly from auth commands and centralise command error handling.
  • Propagate cancellation through command handlers.
  • Exit 130 and print nothing when a run is cancelled, and let a caller pass its own token to RunAsync.
  • Keep prompts on stderr so JSON and raw stdout remain safe for scripts.
  • Replace dynamic pipeline shaping with typed data and preserve the public output shape.
  • Add regression tests for the reviewed issues.

Cancellation, after review feedback

Copilot flagged InvokeAsync(..., default) in Program.cs, reading it as
Ctrl+C never reaching handlers. Ctrl+C did reach them: InvocationPipeline
links the token it is given to a source of its own and cancels it from
ProcessTerminationHandler, which registers for SIGINT and SIGTERM. Two real
problems sat next to it, both fixed in 9b855c9:

  • A caller driving RunAsync could not cancel, since the token was hardcoded.
  • Cancellation worked but did not exit cleanly. The OperationCanceledException
    fell to the catch-all and printed Error: The operation was canceled. with
    exit 1. It now exits 130 and prints nothing. An HttpClient timeout carries a
    TimeoutException inside, so it still reports as a failure with exit 1.

No Console.CancelKeyPress hook was added; it would only compete with the
library's own signal registration.

Validation

  • All 224 unit tests pass.
  • The project builds with zero warnings and zero errors.
  • The new cancellation test fails on the old code and passes on the new.
  • Ctrl+C sent through a real pty mid-request: old build exits 1 with an error
    line, new build exits 130 silently.
  • Read-only commands passed against existing Bitbucket repositories.
  • A full live write test passed in a throwaway Bitbucket repository, including
    upload, streamed download to a file and stdout, and SHA-256 checks.
  • A second throwaway repository covered repo create, src write,
    branch create, pr create, pr decline, branch delete and repo delete.
  • Both test artifacts and throwaway repositories were deleted, and repository
    absence was verified.

Safety

  • No existing Bitbucket repository was changed or deleted.
  • No SSH keys or security settings were changed.
  • The local credential file was not rewritten during live testing.

Keep authentication scoped to the Bitbucket API origin, handle nullable API data safely, stream downloads, preserve clean command output, and store credentials with safer file permissions. Add regression tests for each reviewed issue and propagate command cancellation throughout the CLI.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the CLI’s command boundary and Bitbucket API interactions by centralizing error handling, tightening credential behavior across redirects, improving streaming/encoding robustness, and adding regression tests to prevent regressions in script-safe output.

Changes:

  • Centralize command failure handling at the program boundary (exceptions + consistent non-zero exit codes) and propagate cancellation through handlers.
  • Harden API/IO behaviors: stream downloads, scope credentials to API origin across redirects, atomically persist credentials with strict file permissions, and safely escape endpoint path segments.
  • Improve resilience to nullable/nested API JSON (including pipelines output shaping) and add regression test coverage for the above.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/Bbx.Tests/ProgramTests.csAdds regression coverage for program-boundary error handling and exit codes.
tests/Bbx.Tests/Features/Users/UserHandlersTests.csUpdates user handler tests for revised auth/error behavior.
tests/Bbx.Tests/Features/Snippets/SnippetFilesHandlerTests.csVerifies per-segment path escaping for snippet file paths.
tests/Bbx.Tests/Features/Pipelines/PipelineFormatTests.csValidates pipeline formatting tolerates null nested members and preserves JSON shape.
tests/Bbx.Tests/Features/NullNestedObjectHandlerTests.csEnsures handlers tolerate null nested objects from the API.
tests/Bbx.Tests/Features/Downloads/GetDownloadHandlerTests.csUpdates download handler tests for streaming-to-destination behavior.
tests/Bbx.Tests/Features/Auth/AuthTokenHandlerTests.csUpdates auth token tests for exception-based user errors and return values.
tests/Bbx.Tests/Features/Auth/AuthStatusHandlerTests.csUpdates auth status tests for exception-based failures and API error propagation.
tests/Bbx.Tests/Commands/ConfirmationOutputTests.csEnsures destructive prompts write to stderr (stdout remains script-safe).
tests/Bbx.Tests/Commands/CommandRunnerTests.csUpdates expectations: errors now bubble to the program boundary.
tests/Bbx.Tests/Commands/CommandBindingTests.csVerifies System.CommandLine cancellation token reaches bound handlers.
tests/Bbx.Tests/Auth/FileCredentialStoreTests.csAdds tests for atomic credential writes and strict Unix permissions.
tests/Bbx.Tests/Api/BitbucketClientTests.csAdds redirect/auth-origin and streaming-copy coverage for the API client.
src/Bbx/Program.csCentralizes exception handling for the CLI and configures invocation behavior.
src/Bbx/Features/Workspaces/ViewWorkspace/ViewWorkspaceHandler.csRemoves redundant credential checks; hardens nested JSON handling via TryGetObject.
src/Bbx/Features/Workspaces/ListWorkspaces/ListWorkspacesHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Workspaces/ListWorkspacePermissions/ListWorkspacePermissionsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Workspaces/ListWorkspaceMembers/ListWorkspaceMembersHandler.csUses TryGetObject to avoid null-object pitfalls.
src/Bbx/Features/Users/ViewUser/ViewUserHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ViewSshKey/ViewSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ListSshKeys/ListSshKeysHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/DeleteSshKey/DeleteSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/AddSshKey/AddSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserWorkspacePermissions/ListUserWorkspacePermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserRepositoryPermissions/ListUserRepositoryPermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserEmails/ListUserEmailsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Source/LsSource/LsSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Source/CatSource/CatSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Snippets/ViewSnippet/ViewSnippetHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Snippets/UpdateSnippet/UpdateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetWatch/SnippetWatchHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetFiles/SnippetFilesHandler.csEscapes per-segment file paths while preserving directories.
src/Bbx/Features/Snippets/SnippetComments/SnippetCommentsHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/ListSnippets/ListSnippetsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Snippets/DeleteSnippet/DeleteSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/CreateSnippet/CreateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Repos/RepoPermissions/RepoPermissionsHandler.csAvoids null nested-object access when shaping permissions output.
src/Bbx/Features/Pipelines/ViewPipeline/ViewPipelineHandler.csHardens steps shaping against non-array/null values.
src/Bbx/Features/Pipelines/ViewDeploymentEnvironment/ViewDeploymentEnvironmentHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/PipelineFormat.csReplaces dynamic shaping with typed records while preserving output shape.
src/Bbx/Features/Pipelines/ListPipelineVariables/ListPipelineVariablesHandler.csSafely reads boolean properties without throwing on null/non-bool.
src/Bbx/Features/Pipelines/ListPipelineSchedules/ListPipelineSchedulesHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/ListDeploymentEnvironments/ListDeploymentEnvironmentsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Downloads/GetDownload/GetDownloadHandler.csSwitches download handling to streaming via a destination stream.
src/Bbx/Features/Common/EndpointPath.csIntroduces shared helper for per-segment path escaping.
src/Bbx/Features/Commits/FileHistory/FileHistoryHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Auth/Token/AuthTokenHandler.csReturns token string and throws user errors instead of writing/setting exit code.
src/Bbx/Features/Auth/Status/AuthStatusHandler.csReturns status string and lets API failures propagate to the program boundary.
src/Bbx/Commands/WorkspaceCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/UserCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SrcCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SnippetCommand.csCentralizes direct command error handling and propagates cancellation.
src/Bbx/Commands/RepoCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/PrCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/PipelineCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/IssueCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/DownloadCommand.csStreams downloads to file/stdout with safer temp-file handling and cancellation support.
src/Bbx/Commands/CommitCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/CommandRunner.csRefactors runner helpers to rely on program-boundary exception handling and adds stream runner.
src/Bbx/Commands/CommandBinding.csAdds AsyncLocal cancellation token propagation into handlers.
src/Bbx/Commands/BranchCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/AuthCommand.csSwitches auth commands to throw user errors and standardizes cancellation propagation.
src/Bbx/Auth/FileCredentialStore.csMakes credential load failures explicit; saves atomically with strict Unix permissions.
src/Bbx/Api/BitbucketClient.csAdds streaming CopyToAsync and tightens auth scoping across redirects/origins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Bbx/Program.cs Outdated
Comment on lines +69 to +72
exitCode = await parseResult.InvokeAsync(new InvocationConfiguration
{
EnableDefaultExceptionHandler = false,
}, default);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half right, so I checked it against the library rather than the description. Fixed in 9b855c9.

Ctrl+C already reached handlers.InvocationPipeline.InvokeAsync does CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) and gives the action cts.Token, then hands that same source to ProcessTerminationHandler, which registers for SIGINT and SIGTERM via PosixSignalRegistration on .NET 7+. So the token CommandBinding sees is cancelable even when default goes in, and a signal cancels it.

Verified against the built tool, Ctrl+C sent through a real pty mid-request:

baseline, no Ctrl+C: exit=0, 4988 bytes of JSON
Ctrl+C at 0.4s: exit=130, no output

I did not take the Console.CancelKeyPress suggestion. ProcessTerminationHandler only falls back to CancelKeyPress on platforms without PosixSignalRegistration; adding our own subscriber would be a second handler competing with the library's for the same signal.

Two things were wrong. First, a caller driving RunAsync had no way to cancel, since the token was hardcoded. It now takes an optional CancellationToken.

Second, and more visible: cancellation worked but did not exit cleanly. The OperationCanceledException fell through to the catch-all and printed Error: The operation was canceled. with exit 1. Confirmed on the pre-change build:

OLD build, Ctrl+C at 0.4s: exit=1, "Error: The operation was canceled."
NEW build, Ctrl+C at 0.4s: exit=130, no output

A cancelled run is not an error, so it now exits 130, the shell convention for SIGINT, and prints nothing. An HttpClient timeout also surfaces as an OperationCanceledException, but carries a TimeoutException inside, so the filter lets it through to the failure path and it still exits 1.

Tests.Cancelling_the_supplied_token_stops_the_command_before_it_calls_the_api fails on the old code with Expected exitCode to be 130, but found 0 and passes on the new. Bound_handlers_get_a_cancelable_token_even_with_no_token_supplied pins the library behaviour above, so anyone reading this thread and reaching for a signal hook gets a test explaining why not.

That test needed one test-kit fix: FakeHttpMessageHandler ignored the token. HttpClient passes the token straight to its handler without checking it first, so the fake was answering requests a real handler would have refused.

224 tests pass. Also exercised end to end with the packed tool: read-only commands against real repos, and the write and destructive paths (repo create, src write, branch create, pr create, pr decline, branch delete, repo delete) against a throwaway repo, since deleted.

Program.RunAsync hardcoded `default` as the token for InvokeAsync, so a
caller driving RunAsync had no way to stop a command. It now takes an
optional token and passes it through.
Ctrl+C already reached handlers: System.CommandLine links whatever token
it is given to a source of its own and cancels that from
ProcessTerminationHandler. What it did not do was exit cleanly. The
resulting OperationCanceledException fell to the catch-all and printed
"Error: The operation was canceled." with exit 1. A cancelled run is not
an error, so it now exits 130 and says nothing. An HttpClient timeout
also surfaces as an OperationCanceledException but carries a
TimeoutException inside, so it still reports as a failure with exit 1.
FakeHttpMessageHandler now honours the token. HttpClient hands it to the
handler without checking it first, so a fake that ignored it answered
requests a real handler would have refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.

@solrevdev
solrevdev merged commit 4706ca7 into masterAug 4, 2026
3 checks passed
@solrevdev
solrevdev deleted the fix/repository-review-findings branch August 4, 2026 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@solrevdev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: harden command and API handling - #2

Merged
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings
Aug 4, 2026
Merged

fix: harden command and API handling#2
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings

Conversation

@solrevdev

@solrevdevsolrevdev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep credentials scoped to the Bitbucket API origin across redirects.
  • Stream downloads instead of buffering full responses in memory.
  • Handle nullable nested API data and encode endpoint path segments safely.
  • Save credentials atomically with strict Unix file permissions.
  • Return failures correctly from auth commands and centralise command error handling.
  • Propagate cancellation through command handlers.
  • Exit 130 and print nothing when a run is cancelled, and let a caller pass its own token to RunAsync.
  • Keep prompts on stderr so JSON and raw stdout remain safe for scripts.
  • Replace dynamic pipeline shaping with typed data and preserve the public output shape.
  • Add regression tests for the reviewed issues.

Cancellation, after review feedback

Copilot flagged InvokeAsync(..., default) in Program.cs, reading it as
Ctrl+C never reaching handlers. Ctrl+C did reach them: InvocationPipeline
links the token it is given to a source of its own and cancels it from
ProcessTerminationHandler, which registers for SIGINT and SIGTERM. Two real
problems sat next to it, both fixed in 9b855c9:

  • A caller driving RunAsync could not cancel, since the token was hardcoded.
  • Cancellation worked but did not exit cleanly. The OperationCanceledException
    fell to the catch-all and printed Error: The operation was canceled. with
    exit 1. It now exits 130 and prints nothing. An HttpClient timeout carries a
    TimeoutException inside, so it still reports as a failure with exit 1.

No Console.CancelKeyPress hook was added; it would only compete with the
library's own signal registration.

Validation

  • All 224 unit tests pass.
  • The project builds with zero warnings and zero errors.
  • The new cancellation test fails on the old code and passes on the new.
  • Ctrl+C sent through a real pty mid-request: old build exits 1 with an error
    line, new build exits 130 silently.
  • Read-only commands passed against existing Bitbucket repositories.
  • A full live write test passed in a throwaway Bitbucket repository, including
    upload, streamed download to a file and stdout, and SHA-256 checks.
  • A second throwaway repository covered repo create, src write,
    branch create, pr create, pr decline, branch delete and repo delete.
  • Both test artifacts and throwaway repositories were deleted, and repository
    absence was verified.

Safety

  • No existing Bitbucket repository was changed or deleted.
  • No SSH keys or security settings were changed.
  • The local credential file was not rewritten during live testing.

Keep authentication scoped to the Bitbucket API origin, handle nullable API data safely, stream downloads, preserve clean command output, and store credentials with safer file permissions. Add regression tests for each reviewed issue and propagate command cancellation throughout the CLI.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the CLI’s command boundary and Bitbucket API interactions by centralizing error handling, tightening credential behavior across redirects, improving streaming/encoding robustness, and adding regression tests to prevent regressions in script-safe output.

Changes:

  • Centralize command failure handling at the program boundary (exceptions + consistent non-zero exit codes) and propagate cancellation through handlers.
  • Harden API/IO behaviors: stream downloads, scope credentials to API origin across redirects, atomically persist credentials with strict file permissions, and safely escape endpoint path segments.
  • Improve resilience to nullable/nested API JSON (including pipelines output shaping) and add regression test coverage for the above.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/Bbx.Tests/ProgramTests.csAdds regression coverage for program-boundary error handling and exit codes.
tests/Bbx.Tests/Features/Users/UserHandlersTests.csUpdates user handler tests for revised auth/error behavior.
tests/Bbx.Tests/Features/Snippets/SnippetFilesHandlerTests.csVerifies per-segment path escaping for snippet file paths.
tests/Bbx.Tests/Features/Pipelines/PipelineFormatTests.csValidates pipeline formatting tolerates null nested members and preserves JSON shape.
tests/Bbx.Tests/Features/NullNestedObjectHandlerTests.csEnsures handlers tolerate null nested objects from the API.
tests/Bbx.Tests/Features/Downloads/GetDownloadHandlerTests.csUpdates download handler tests for streaming-to-destination behavior.
tests/Bbx.Tests/Features/Auth/AuthTokenHandlerTests.csUpdates auth token tests for exception-based user errors and return values.
tests/Bbx.Tests/Features/Auth/AuthStatusHandlerTests.csUpdates auth status tests for exception-based failures and API error propagation.
tests/Bbx.Tests/Commands/ConfirmationOutputTests.csEnsures destructive prompts write to stderr (stdout remains script-safe).
tests/Bbx.Tests/Commands/CommandRunnerTests.csUpdates expectations: errors now bubble to the program boundary.
tests/Bbx.Tests/Commands/CommandBindingTests.csVerifies System.CommandLine cancellation token reaches bound handlers.
tests/Bbx.Tests/Auth/FileCredentialStoreTests.csAdds tests for atomic credential writes and strict Unix permissions.
tests/Bbx.Tests/Api/BitbucketClientTests.csAdds redirect/auth-origin and streaming-copy coverage for the API client.
src/Bbx/Program.csCentralizes exception handling for the CLI and configures invocation behavior.
src/Bbx/Features/Workspaces/ViewWorkspace/ViewWorkspaceHandler.csRemoves redundant credential checks; hardens nested JSON handling via TryGetObject.
src/Bbx/Features/Workspaces/ListWorkspaces/ListWorkspacesHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Workspaces/ListWorkspacePermissions/ListWorkspacePermissionsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Workspaces/ListWorkspaceMembers/ListWorkspaceMembersHandler.csUses TryGetObject to avoid null-object pitfalls.
src/Bbx/Features/Users/ViewUser/ViewUserHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ViewSshKey/ViewSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ListSshKeys/ListSshKeysHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/DeleteSshKey/DeleteSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/AddSshKey/AddSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserWorkspacePermissions/ListUserWorkspacePermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserRepositoryPermissions/ListUserRepositoryPermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserEmails/ListUserEmailsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Source/LsSource/LsSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Source/CatSource/CatSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Snippets/ViewSnippet/ViewSnippetHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Snippets/UpdateSnippet/UpdateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetWatch/SnippetWatchHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetFiles/SnippetFilesHandler.csEscapes per-segment file paths while preserving directories.
src/Bbx/Features/Snippets/SnippetComments/SnippetCommentsHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/ListSnippets/ListSnippetsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Snippets/DeleteSnippet/DeleteSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/CreateSnippet/CreateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Repos/RepoPermissions/RepoPermissionsHandler.csAvoids null nested-object access when shaping permissions output.
src/Bbx/Features/Pipelines/ViewPipeline/ViewPipelineHandler.csHardens steps shaping against non-array/null values.
src/Bbx/Features/Pipelines/ViewDeploymentEnvironment/ViewDeploymentEnvironmentHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/PipelineFormat.csReplaces dynamic shaping with typed records while preserving output shape.
src/Bbx/Features/Pipelines/ListPipelineVariables/ListPipelineVariablesHandler.csSafely reads boolean properties without throwing on null/non-bool.
src/Bbx/Features/Pipelines/ListPipelineSchedules/ListPipelineSchedulesHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/ListDeploymentEnvironments/ListDeploymentEnvironmentsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Downloads/GetDownload/GetDownloadHandler.csSwitches download handling to streaming via a destination stream.
src/Bbx/Features/Common/EndpointPath.csIntroduces shared helper for per-segment path escaping.
src/Bbx/Features/Commits/FileHistory/FileHistoryHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Auth/Token/AuthTokenHandler.csReturns token string and throws user errors instead of writing/setting exit code.
src/Bbx/Features/Auth/Status/AuthStatusHandler.csReturns status string and lets API failures propagate to the program boundary.
src/Bbx/Commands/WorkspaceCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/UserCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SrcCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SnippetCommand.csCentralizes direct command error handling and propagates cancellation.
src/Bbx/Commands/RepoCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/PrCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/PipelineCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/IssueCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/DownloadCommand.csStreams downloads to file/stdout with safer temp-file handling and cancellation support.
src/Bbx/Commands/CommitCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/CommandRunner.csRefactors runner helpers to rely on program-boundary exception handling and adds stream runner.
src/Bbx/Commands/CommandBinding.csAdds AsyncLocal cancellation token propagation into handlers.
src/Bbx/Commands/BranchCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/AuthCommand.csSwitches auth commands to throw user errors and standardizes cancellation propagation.
src/Bbx/Auth/FileCredentialStore.csMakes credential load failures explicit; saves atomically with strict Unix permissions.
src/Bbx/Api/BitbucketClient.csAdds streaming CopyToAsync and tightens auth scoping across redirects/origins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Bbx/Program.cs Outdated
Comment on lines +69 to +72
exitCode = await parseResult.InvokeAsync(new InvocationConfiguration
{
EnableDefaultExceptionHandler = false,
}, default);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half right, so I checked it against the library rather than the description. Fixed in 9b855c9.

Ctrl+C already reached handlers.InvocationPipeline.InvokeAsync does CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) and gives the action cts.Token, then hands that same source to ProcessTerminationHandler, which registers for SIGINT and SIGTERM via PosixSignalRegistration on .NET 7+. So the token CommandBinding sees is cancelable even when default goes in, and a signal cancels it.

Verified against the built tool, Ctrl+C sent through a real pty mid-request:

baseline, no Ctrl+C: exit=0, 4988 bytes of JSON
Ctrl+C at 0.4s: exit=130, no output

I did not take the Console.CancelKeyPress suggestion. ProcessTerminationHandler only falls back to CancelKeyPress on platforms without PosixSignalRegistration; adding our own subscriber would be a second handler competing with the library's for the same signal.

Two things were wrong. First, a caller driving RunAsync had no way to cancel, since the token was hardcoded. It now takes an optional CancellationToken.

Second, and more visible: cancellation worked but did not exit cleanly. The OperationCanceledException fell through to the catch-all and printed Error: The operation was canceled. with exit 1. Confirmed on the pre-change build:

OLD build, Ctrl+C at 0.4s: exit=1, "Error: The operation was canceled."
NEW build, Ctrl+C at 0.4s: exit=130, no output

A cancelled run is not an error, so it now exits 130, the shell convention for SIGINT, and prints nothing. An HttpClient timeout also surfaces as an OperationCanceledException, but carries a TimeoutException inside, so the filter lets it through to the failure path and it still exits 1.

Tests.Cancelling_the_supplied_token_stops_the_command_before_it_calls_the_api fails on the old code with Expected exitCode to be 130, but found 0 and passes on the new. Bound_handlers_get_a_cancelable_token_even_with_no_token_supplied pins the library behaviour above, so anyone reading this thread and reaching for a signal hook gets a test explaining why not.

That test needed one test-kit fix: FakeHttpMessageHandler ignored the token. HttpClient passes the token straight to its handler without checking it first, so the fake was answering requests a real handler would have refused.

224 tests pass. Also exercised end to end with the packed tool: read-only commands against real repos, and the write and destructive paths (repo create, src write, branch create, pr create, pr decline, branch delete, repo delete) against a throwaway repo, since deleted.

Program.RunAsync hardcoded `default` as the token for InvokeAsync, so a
caller driving RunAsync had no way to stop a command. It now takes an
optional token and passes it through.
Ctrl+C already reached handlers: System.CommandLine links whatever token
it is given to a source of its own and cancels that from
ProcessTerminationHandler. What it did not do was exit cleanly. The
resulting OperationCanceledException fell to the catch-all and printed
"Error: The operation was canceled." with exit 1. A cancelled run is not
an error, so it now exits 130 and says nothing. An HttpClient timeout
also surfaces as an OperationCanceledException but carries a
TimeoutException inside, so it still reports as a failure with exit 1.
FakeHttpMessageHandler now honours the token. HttpClient hands it to the
handler without checking it first, so a fake that ignored it answered
requests a real handler would have refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.

@solrevdev
solrevdev merged commit 4706ca7 into masterAug 4, 2026
3 checks passed
@solrevdev
solrevdev deleted the fix/repository-review-findings branch August 4, 2026 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@solrevdev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: harden command and API handling - #2

Merged
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings
Aug 4, 2026
Merged

fix: harden command and API handling#2
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings

Conversation

@solrevdev

@solrevdevsolrevdev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep credentials scoped to the Bitbucket API origin across redirects.
  • Stream downloads instead of buffering full responses in memory.
  • Handle nullable nested API data and encode endpoint path segments safely.
  • Save credentials atomically with strict Unix file permissions.
  • Return failures correctly from auth commands and centralise command error handling.
  • Propagate cancellation through command handlers.
  • Exit 130 and print nothing when a run is cancelled, and let a caller pass its own token to RunAsync.
  • Keep prompts on stderr so JSON and raw stdout remain safe for scripts.
  • Replace dynamic pipeline shaping with typed data and preserve the public output shape.
  • Add regression tests for the reviewed issues.

Cancellation, after review feedback

Copilot flagged InvokeAsync(..., default) in Program.cs, reading it as
Ctrl+C never reaching handlers. Ctrl+C did reach them: InvocationPipeline
links the token it is given to a source of its own and cancels it from
ProcessTerminationHandler, which registers for SIGINT and SIGTERM. Two real
problems sat next to it, both fixed in 9b855c9:

  • A caller driving RunAsync could not cancel, since the token was hardcoded.
  • Cancellation worked but did not exit cleanly. The OperationCanceledException
    fell to the catch-all and printed Error: The operation was canceled. with
    exit 1. It now exits 130 and prints nothing. An HttpClient timeout carries a
    TimeoutException inside, so it still reports as a failure with exit 1.

No Console.CancelKeyPress hook was added; it would only compete with the
library's own signal registration.

Validation

  • All 224 unit tests pass.
  • The project builds with zero warnings and zero errors.
  • The new cancellation test fails on the old code and passes on the new.
  • Ctrl+C sent through a real pty mid-request: old build exits 1 with an error
    line, new build exits 130 silently.
  • Read-only commands passed against existing Bitbucket repositories.
  • A full live write test passed in a throwaway Bitbucket repository, including
    upload, streamed download to a file and stdout, and SHA-256 checks.
  • A second throwaway repository covered repo create, src write,
    branch create, pr create, pr decline, branch delete and repo delete.
  • Both test artifacts and throwaway repositories were deleted, and repository
    absence was verified.

Safety

  • No existing Bitbucket repository was changed or deleted.
  • No SSH keys or security settings were changed.
  • The local credential file was not rewritten during live testing.

Keep authentication scoped to the Bitbucket API origin, handle nullable API data safely, stream downloads, preserve clean command output, and store credentials with safer file permissions. Add regression tests for each reviewed issue and propagate command cancellation throughout the CLI.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the CLI’s command boundary and Bitbucket API interactions by centralizing error handling, tightening credential behavior across redirects, improving streaming/encoding robustness, and adding regression tests to prevent regressions in script-safe output.

Changes:

  • Centralize command failure handling at the program boundary (exceptions + consistent non-zero exit codes) and propagate cancellation through handlers.
  • Harden API/IO behaviors: stream downloads, scope credentials to API origin across redirects, atomically persist credentials with strict file permissions, and safely escape endpoint path segments.
  • Improve resilience to nullable/nested API JSON (including pipelines output shaping) and add regression test coverage for the above.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/Bbx.Tests/ProgramTests.csAdds regression coverage for program-boundary error handling and exit codes.
tests/Bbx.Tests/Features/Users/UserHandlersTests.csUpdates user handler tests for revised auth/error behavior.
tests/Bbx.Tests/Features/Snippets/SnippetFilesHandlerTests.csVerifies per-segment path escaping for snippet file paths.
tests/Bbx.Tests/Features/Pipelines/PipelineFormatTests.csValidates pipeline formatting tolerates null nested members and preserves JSON shape.
tests/Bbx.Tests/Features/NullNestedObjectHandlerTests.csEnsures handlers tolerate null nested objects from the API.
tests/Bbx.Tests/Features/Downloads/GetDownloadHandlerTests.csUpdates download handler tests for streaming-to-destination behavior.
tests/Bbx.Tests/Features/Auth/AuthTokenHandlerTests.csUpdates auth token tests for exception-based user errors and return values.
tests/Bbx.Tests/Features/Auth/AuthStatusHandlerTests.csUpdates auth status tests for exception-based failures and API error propagation.
tests/Bbx.Tests/Commands/ConfirmationOutputTests.csEnsures destructive prompts write to stderr (stdout remains script-safe).
tests/Bbx.Tests/Commands/CommandRunnerTests.csUpdates expectations: errors now bubble to the program boundary.
tests/Bbx.Tests/Commands/CommandBindingTests.csVerifies System.CommandLine cancellation token reaches bound handlers.
tests/Bbx.Tests/Auth/FileCredentialStoreTests.csAdds tests for atomic credential writes and strict Unix permissions.
tests/Bbx.Tests/Api/BitbucketClientTests.csAdds redirect/auth-origin and streaming-copy coverage for the API client.
src/Bbx/Program.csCentralizes exception handling for the CLI and configures invocation behavior.
src/Bbx/Features/Workspaces/ViewWorkspace/ViewWorkspaceHandler.csRemoves redundant credential checks; hardens nested JSON handling via TryGetObject.
src/Bbx/Features/Workspaces/ListWorkspaces/ListWorkspacesHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Workspaces/ListWorkspacePermissions/ListWorkspacePermissionsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Workspaces/ListWorkspaceMembers/ListWorkspaceMembersHandler.csUses TryGetObject to avoid null-object pitfalls.
src/Bbx/Features/Users/ViewUser/ViewUserHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ViewSshKey/ViewSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ListSshKeys/ListSshKeysHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/DeleteSshKey/DeleteSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/AddSshKey/AddSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserWorkspacePermissions/ListUserWorkspacePermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserRepositoryPermissions/ListUserRepositoryPermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserEmails/ListUserEmailsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Source/LsSource/LsSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Source/CatSource/CatSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Snippets/ViewSnippet/ViewSnippetHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Snippets/UpdateSnippet/UpdateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetWatch/SnippetWatchHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetFiles/SnippetFilesHandler.csEscapes per-segment file paths while preserving directories.
src/Bbx/Features/Snippets/SnippetComments/SnippetCommentsHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/ListSnippets/ListSnippetsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Snippets/DeleteSnippet/DeleteSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/CreateSnippet/CreateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Repos/RepoPermissions/RepoPermissionsHandler.csAvoids null nested-object access when shaping permissions output.
src/Bbx/Features/Pipelines/ViewPipeline/ViewPipelineHandler.csHardens steps shaping against non-array/null values.
src/Bbx/Features/Pipelines/ViewDeploymentEnvironment/ViewDeploymentEnvironmentHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/PipelineFormat.csReplaces dynamic shaping with typed records while preserving output shape.
src/Bbx/Features/Pipelines/ListPipelineVariables/ListPipelineVariablesHandler.csSafely reads boolean properties without throwing on null/non-bool.
src/Bbx/Features/Pipelines/ListPipelineSchedules/ListPipelineSchedulesHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/ListDeploymentEnvironments/ListDeploymentEnvironmentsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Downloads/GetDownload/GetDownloadHandler.csSwitches download handling to streaming via a destination stream.
src/Bbx/Features/Common/EndpointPath.csIntroduces shared helper for per-segment path escaping.
src/Bbx/Features/Commits/FileHistory/FileHistoryHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Auth/Token/AuthTokenHandler.csReturns token string and throws user errors instead of writing/setting exit code.
src/Bbx/Features/Auth/Status/AuthStatusHandler.csReturns status string and lets API failures propagate to the program boundary.
src/Bbx/Commands/WorkspaceCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/UserCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SrcCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SnippetCommand.csCentralizes direct command error handling and propagates cancellation.
src/Bbx/Commands/RepoCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/PrCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/PipelineCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/IssueCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/DownloadCommand.csStreams downloads to file/stdout with safer temp-file handling and cancellation support.
src/Bbx/Commands/CommitCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/CommandRunner.csRefactors runner helpers to rely on program-boundary exception handling and adds stream runner.
src/Bbx/Commands/CommandBinding.csAdds AsyncLocal cancellation token propagation into handlers.
src/Bbx/Commands/BranchCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/AuthCommand.csSwitches auth commands to throw user errors and standardizes cancellation propagation.
src/Bbx/Auth/FileCredentialStore.csMakes credential load failures explicit; saves atomically with strict Unix permissions.
src/Bbx/Api/BitbucketClient.csAdds streaming CopyToAsync and tightens auth scoping across redirects/origins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Bbx/Program.cs Outdated
Comment on lines +69 to +72
exitCode = await parseResult.InvokeAsync(new InvocationConfiguration
{
EnableDefaultExceptionHandler = false,
}, default);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half right, so I checked it against the library rather than the description. Fixed in 9b855c9.

Ctrl+C already reached handlers.InvocationPipeline.InvokeAsync does CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) and gives the action cts.Token, then hands that same source to ProcessTerminationHandler, which registers for SIGINT and SIGTERM via PosixSignalRegistration on .NET 7+. So the token CommandBinding sees is cancelable even when default goes in, and a signal cancels it.

Verified against the built tool, Ctrl+C sent through a real pty mid-request:

baseline, no Ctrl+C: exit=0, 4988 bytes of JSON
Ctrl+C at 0.4s: exit=130, no output

I did not take the Console.CancelKeyPress suggestion. ProcessTerminationHandler only falls back to CancelKeyPress on platforms without PosixSignalRegistration; adding our own subscriber would be a second handler competing with the library's for the same signal.

Two things were wrong. First, a caller driving RunAsync had no way to cancel, since the token was hardcoded. It now takes an optional CancellationToken.

Second, and more visible: cancellation worked but did not exit cleanly. The OperationCanceledException fell through to the catch-all and printed Error: The operation was canceled. with exit 1. Confirmed on the pre-change build:

OLD build, Ctrl+C at 0.4s: exit=1, "Error: The operation was canceled."
NEW build, Ctrl+C at 0.4s: exit=130, no output

A cancelled run is not an error, so it now exits 130, the shell convention for SIGINT, and prints nothing. An HttpClient timeout also surfaces as an OperationCanceledException, but carries a TimeoutException inside, so the filter lets it through to the failure path and it still exits 1.

Tests.Cancelling_the_supplied_token_stops_the_command_before_it_calls_the_api fails on the old code with Expected exitCode to be 130, but found 0 and passes on the new. Bound_handlers_get_a_cancelable_token_even_with_no_token_supplied pins the library behaviour above, so anyone reading this thread and reaching for a signal hook gets a test explaining why not.

That test needed one test-kit fix: FakeHttpMessageHandler ignored the token. HttpClient passes the token straight to its handler without checking it first, so the fake was answering requests a real handler would have refused.

224 tests pass. Also exercised end to end with the packed tool: read-only commands against real repos, and the write and destructive paths (repo create, src write, branch create, pr create, pr decline, branch delete, repo delete) against a throwaway repo, since deleted.

Program.RunAsync hardcoded `default` as the token for InvokeAsync, so a
caller driving RunAsync had no way to stop a command. It now takes an
optional token and passes it through.
Ctrl+C already reached handlers: System.CommandLine links whatever token
it is given to a source of its own and cancels that from
ProcessTerminationHandler. What it did not do was exit cleanly. The
resulting OperationCanceledException fell to the catch-all and printed
"Error: The operation was canceled." with exit 1. A cancelled run is not
an error, so it now exits 130 and says nothing. An HttpClient timeout
also surfaces as an OperationCanceledException but carries a
TimeoutException inside, so it still reports as a failure with exit 1.
FakeHttpMessageHandler now honours the token. HttpClient hands it to the
handler without checking it first, so a fake that ignored it answered
requests a real handler would have refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.

@solrevdev
solrevdev merged commit 4706ca7 into masterAug 4, 2026
3 checks passed
@solrevdev
solrevdev deleted the fix/repository-review-findings branch August 4, 2026 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@solrevdev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: harden command and API handling - #2

Merged
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings
Aug 4, 2026
Merged

fix: harden command and API handling#2
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings

Conversation

@solrevdev

@solrevdevsolrevdev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep credentials scoped to the Bitbucket API origin across redirects.
  • Stream downloads instead of buffering full responses in memory.
  • Handle nullable nested API data and encode endpoint path segments safely.
  • Save credentials atomically with strict Unix file permissions.
  • Return failures correctly from auth commands and centralise command error handling.
  • Propagate cancellation through command handlers.
  • Exit 130 and print nothing when a run is cancelled, and let a caller pass its own token to RunAsync.
  • Keep prompts on stderr so JSON and raw stdout remain safe for scripts.
  • Replace dynamic pipeline shaping with typed data and preserve the public output shape.
  • Add regression tests for the reviewed issues.

Cancellation, after review feedback

Copilot flagged InvokeAsync(..., default) in Program.cs, reading it as
Ctrl+C never reaching handlers. Ctrl+C did reach them: InvocationPipeline
links the token it is given to a source of its own and cancels it from
ProcessTerminationHandler, which registers for SIGINT and SIGTERM. Two real
problems sat next to it, both fixed in 9b855c9:

  • A caller driving RunAsync could not cancel, since the token was hardcoded.
  • Cancellation worked but did not exit cleanly. The OperationCanceledException
    fell to the catch-all and printed Error: The operation was canceled. with
    exit 1. It now exits 130 and prints nothing. An HttpClient timeout carries a
    TimeoutException inside, so it still reports as a failure with exit 1.

No Console.CancelKeyPress hook was added; it would only compete with the
library's own signal registration.

Validation

  • All 224 unit tests pass.
  • The project builds with zero warnings and zero errors.
  • The new cancellation test fails on the old code and passes on the new.
  • Ctrl+C sent through a real pty mid-request: old build exits 1 with an error
    line, new build exits 130 silently.
  • Read-only commands passed against existing Bitbucket repositories.
  • A full live write test passed in a throwaway Bitbucket repository, including
    upload, streamed download to a file and stdout, and SHA-256 checks.
  • A second throwaway repository covered repo create, src write,
    branch create, pr create, pr decline, branch delete and repo delete.
  • Both test artifacts and throwaway repositories were deleted, and repository
    absence was verified.

Safety

  • No existing Bitbucket repository was changed or deleted.
  • No SSH keys or security settings were changed.
  • The local credential file was not rewritten during live testing.

Keep authentication scoped to the Bitbucket API origin, handle nullable API data safely, stream downloads, preserve clean command output, and store credentials with safer file permissions. Add regression tests for each reviewed issue and propagate command cancellation throughout the CLI.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the CLI’s command boundary and Bitbucket API interactions by centralizing error handling, tightening credential behavior across redirects, improving streaming/encoding robustness, and adding regression tests to prevent regressions in script-safe output.

Changes:

  • Centralize command failure handling at the program boundary (exceptions + consistent non-zero exit codes) and propagate cancellation through handlers.
  • Harden API/IO behaviors: stream downloads, scope credentials to API origin across redirects, atomically persist credentials with strict file permissions, and safely escape endpoint path segments.
  • Improve resilience to nullable/nested API JSON (including pipelines output shaping) and add regression test coverage for the above.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/Bbx.Tests/ProgramTests.csAdds regression coverage for program-boundary error handling and exit codes.
tests/Bbx.Tests/Features/Users/UserHandlersTests.csUpdates user handler tests for revised auth/error behavior.
tests/Bbx.Tests/Features/Snippets/SnippetFilesHandlerTests.csVerifies per-segment path escaping for snippet file paths.
tests/Bbx.Tests/Features/Pipelines/PipelineFormatTests.csValidates pipeline formatting tolerates null nested members and preserves JSON shape.
tests/Bbx.Tests/Features/NullNestedObjectHandlerTests.csEnsures handlers tolerate null nested objects from the API.
tests/Bbx.Tests/Features/Downloads/GetDownloadHandlerTests.csUpdates download handler tests for streaming-to-destination behavior.
tests/Bbx.Tests/Features/Auth/AuthTokenHandlerTests.csUpdates auth token tests for exception-based user errors and return values.
tests/Bbx.Tests/Features/Auth/AuthStatusHandlerTests.csUpdates auth status tests for exception-based failures and API error propagation.
tests/Bbx.Tests/Commands/ConfirmationOutputTests.csEnsures destructive prompts write to stderr (stdout remains script-safe).
tests/Bbx.Tests/Commands/CommandRunnerTests.csUpdates expectations: errors now bubble to the program boundary.
tests/Bbx.Tests/Commands/CommandBindingTests.csVerifies System.CommandLine cancellation token reaches bound handlers.
tests/Bbx.Tests/Auth/FileCredentialStoreTests.csAdds tests for atomic credential writes and strict Unix permissions.
tests/Bbx.Tests/Api/BitbucketClientTests.csAdds redirect/auth-origin and streaming-copy coverage for the API client.
src/Bbx/Program.csCentralizes exception handling for the CLI and configures invocation behavior.
src/Bbx/Features/Workspaces/ViewWorkspace/ViewWorkspaceHandler.csRemoves redundant credential checks; hardens nested JSON handling via TryGetObject.
src/Bbx/Features/Workspaces/ListWorkspaces/ListWorkspacesHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Workspaces/ListWorkspacePermissions/ListWorkspacePermissionsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Workspaces/ListWorkspaceMembers/ListWorkspaceMembersHandler.csUses TryGetObject to avoid null-object pitfalls.
src/Bbx/Features/Users/ViewUser/ViewUserHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ViewSshKey/ViewSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ListSshKeys/ListSshKeysHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/DeleteSshKey/DeleteSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/AddSshKey/AddSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserWorkspacePermissions/ListUserWorkspacePermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserRepositoryPermissions/ListUserRepositoryPermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserEmails/ListUserEmailsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Source/LsSource/LsSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Source/CatSource/CatSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Snippets/ViewSnippet/ViewSnippetHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Snippets/UpdateSnippet/UpdateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetWatch/SnippetWatchHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetFiles/SnippetFilesHandler.csEscapes per-segment file paths while preserving directories.
src/Bbx/Features/Snippets/SnippetComments/SnippetCommentsHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/ListSnippets/ListSnippetsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Snippets/DeleteSnippet/DeleteSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/CreateSnippet/CreateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Repos/RepoPermissions/RepoPermissionsHandler.csAvoids null nested-object access when shaping permissions output.
src/Bbx/Features/Pipelines/ViewPipeline/ViewPipelineHandler.csHardens steps shaping against non-array/null values.
src/Bbx/Features/Pipelines/ViewDeploymentEnvironment/ViewDeploymentEnvironmentHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/PipelineFormat.csReplaces dynamic shaping with typed records while preserving output shape.
src/Bbx/Features/Pipelines/ListPipelineVariables/ListPipelineVariablesHandler.csSafely reads boolean properties without throwing on null/non-bool.
src/Bbx/Features/Pipelines/ListPipelineSchedules/ListPipelineSchedulesHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/ListDeploymentEnvironments/ListDeploymentEnvironmentsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Downloads/GetDownload/GetDownloadHandler.csSwitches download handling to streaming via a destination stream.
src/Bbx/Features/Common/EndpointPath.csIntroduces shared helper for per-segment path escaping.
src/Bbx/Features/Commits/FileHistory/FileHistoryHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Auth/Token/AuthTokenHandler.csReturns token string and throws user errors instead of writing/setting exit code.
src/Bbx/Features/Auth/Status/AuthStatusHandler.csReturns status string and lets API failures propagate to the program boundary.
src/Bbx/Commands/WorkspaceCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/UserCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SrcCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SnippetCommand.csCentralizes direct command error handling and propagates cancellation.
src/Bbx/Commands/RepoCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/PrCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/PipelineCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/IssueCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/DownloadCommand.csStreams downloads to file/stdout with safer temp-file handling and cancellation support.
src/Bbx/Commands/CommitCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/CommandRunner.csRefactors runner helpers to rely on program-boundary exception handling and adds stream runner.
src/Bbx/Commands/CommandBinding.csAdds AsyncLocal cancellation token propagation into handlers.
src/Bbx/Commands/BranchCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/AuthCommand.csSwitches auth commands to throw user errors and standardizes cancellation propagation.
src/Bbx/Auth/FileCredentialStore.csMakes credential load failures explicit; saves atomically with strict Unix permissions.
src/Bbx/Api/BitbucketClient.csAdds streaming CopyToAsync and tightens auth scoping across redirects/origins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Bbx/Program.cs Outdated
Comment on lines +69 to +72
exitCode = await parseResult.InvokeAsync(new InvocationConfiguration
{
EnableDefaultExceptionHandler = false,
}, default);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half right, so I checked it against the library rather than the description. Fixed in 9b855c9.

Ctrl+C already reached handlers.InvocationPipeline.InvokeAsync does CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) and gives the action cts.Token, then hands that same source to ProcessTerminationHandler, which registers for SIGINT and SIGTERM via PosixSignalRegistration on .NET 7+. So the token CommandBinding sees is cancelable even when default goes in, and a signal cancels it.

Verified against the built tool, Ctrl+C sent through a real pty mid-request:

baseline, no Ctrl+C: exit=0, 4988 bytes of JSON
Ctrl+C at 0.4s: exit=130, no output

I did not take the Console.CancelKeyPress suggestion. ProcessTerminationHandler only falls back to CancelKeyPress on platforms without PosixSignalRegistration; adding our own subscriber would be a second handler competing with the library's for the same signal.

Two things were wrong. First, a caller driving RunAsync had no way to cancel, since the token was hardcoded. It now takes an optional CancellationToken.

Second, and more visible: cancellation worked but did not exit cleanly. The OperationCanceledException fell through to the catch-all and printed Error: The operation was canceled. with exit 1. Confirmed on the pre-change build:

OLD build, Ctrl+C at 0.4s: exit=1, "Error: The operation was canceled."
NEW build, Ctrl+C at 0.4s: exit=130, no output

A cancelled run is not an error, so it now exits 130, the shell convention for SIGINT, and prints nothing. An HttpClient timeout also surfaces as an OperationCanceledException, but carries a TimeoutException inside, so the filter lets it through to the failure path and it still exits 1.

Tests.Cancelling_the_supplied_token_stops_the_command_before_it_calls_the_api fails on the old code with Expected exitCode to be 130, but found 0 and passes on the new. Bound_handlers_get_a_cancelable_token_even_with_no_token_supplied pins the library behaviour above, so anyone reading this thread and reaching for a signal hook gets a test explaining why not.

That test needed one test-kit fix: FakeHttpMessageHandler ignored the token. HttpClient passes the token straight to its handler without checking it first, so the fake was answering requests a real handler would have refused.

224 tests pass. Also exercised end to end with the packed tool: read-only commands against real repos, and the write and destructive paths (repo create, src write, branch create, pr create, pr decline, branch delete, repo delete) against a throwaway repo, since deleted.

Program.RunAsync hardcoded `default` as the token for InvokeAsync, so a
caller driving RunAsync had no way to stop a command. It now takes an
optional token and passes it through.
Ctrl+C already reached handlers: System.CommandLine links whatever token
it is given to a source of its own and cancels that from
ProcessTerminationHandler. What it did not do was exit cleanly. The
resulting OperationCanceledException fell to the catch-all and printed
"Error: The operation was canceled." with exit 1. A cancelled run is not
an error, so it now exits 130 and says nothing. An HttpClient timeout
also surfaces as an OperationCanceledException but carries a
TimeoutException inside, so it still reports as a failure with exit 1.
FakeHttpMessageHandler now honours the token. HttpClient hands it to the
handler without checking it first, so a fake that ignored it answered
requests a real handler would have refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.

@solrevdev
solrevdev merged commit 4706ca7 into masterAug 4, 2026
3 checks passed
@solrevdev
solrevdev deleted the fix/repository-review-findings branch August 4, 2026 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@solrevdev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: harden command and API handling - #2

Merged
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings
Aug 4, 2026
Merged

fix: harden command and API handling#2
solrevdev merged 2 commits into
masterfrom
fix/repository-review-findings

Conversation

@solrevdev

@solrevdevsolrevdev commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Keep credentials scoped to the Bitbucket API origin across redirects.
  • Stream downloads instead of buffering full responses in memory.
  • Handle nullable nested API data and encode endpoint path segments safely.
  • Save credentials atomically with strict Unix file permissions.
  • Return failures correctly from auth commands and centralise command error handling.
  • Propagate cancellation through command handlers.
  • Exit 130 and print nothing when a run is cancelled, and let a caller pass its own token to RunAsync.
  • Keep prompts on stderr so JSON and raw stdout remain safe for scripts.
  • Replace dynamic pipeline shaping with typed data and preserve the public output shape.
  • Add regression tests for the reviewed issues.

Cancellation, after review feedback

Copilot flagged InvokeAsync(..., default) in Program.cs, reading it as
Ctrl+C never reaching handlers. Ctrl+C did reach them: InvocationPipeline
links the token it is given to a source of its own and cancels it from
ProcessTerminationHandler, which registers for SIGINT and SIGTERM. Two real
problems sat next to it, both fixed in 9b855c9:

  • A caller driving RunAsync could not cancel, since the token was hardcoded.
  • Cancellation worked but did not exit cleanly. The OperationCanceledException
    fell to the catch-all and printed Error: The operation was canceled. with
    exit 1. It now exits 130 and prints nothing. An HttpClient timeout carries a
    TimeoutException inside, so it still reports as a failure with exit 1.

No Console.CancelKeyPress hook was added; it would only compete with the
library's own signal registration.

Validation

  • All 224 unit tests pass.
  • The project builds with zero warnings and zero errors.
  • The new cancellation test fails on the old code and passes on the new.
  • Ctrl+C sent through a real pty mid-request: old build exits 1 with an error
    line, new build exits 130 silently.
  • Read-only commands passed against existing Bitbucket repositories.
  • A full live write test passed in a throwaway Bitbucket repository, including
    upload, streamed download to a file and stdout, and SHA-256 checks.
  • A second throwaway repository covered repo create, src write,
    branch create, pr create, pr decline, branch delete and repo delete.
  • Both test artifacts and throwaway repositories were deleted, and repository
    absence was verified.

Safety

  • No existing Bitbucket repository was changed or deleted.
  • No SSH keys or security settings were changed.
  • The local credential file was not rewritten during live testing.

Keep authentication scoped to the Bitbucket API origin, handle nullable API data safely, stream downloads, preserve clean command output, and store credentials with safer file permissions. Add regression tests for each reviewed issue and propagate command cancellation throughout the CLI.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the CLI’s command boundary and Bitbucket API interactions by centralizing error handling, tightening credential behavior across redirects, improving streaming/encoding robustness, and adding regression tests to prevent regressions in script-safe output.

Changes:

  • Centralize command failure handling at the program boundary (exceptions + consistent non-zero exit codes) and propagate cancellation through handlers.
  • Harden API/IO behaviors: stream downloads, scope credentials to API origin across redirects, atomically persist credentials with strict file permissions, and safely escape endpoint path segments.
  • Improve resilience to nullable/nested API JSON (including pipelines output shaping) and add regression test coverage for the above.

Reviewed changes

Copilot reviewed 64 out of 64 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
tests/Bbx.Tests/ProgramTests.csAdds regression coverage for program-boundary error handling and exit codes.
tests/Bbx.Tests/Features/Users/UserHandlersTests.csUpdates user handler tests for revised auth/error behavior.
tests/Bbx.Tests/Features/Snippets/SnippetFilesHandlerTests.csVerifies per-segment path escaping for snippet file paths.
tests/Bbx.Tests/Features/Pipelines/PipelineFormatTests.csValidates pipeline formatting tolerates null nested members and preserves JSON shape.
tests/Bbx.Tests/Features/NullNestedObjectHandlerTests.csEnsures handlers tolerate null nested objects from the API.
tests/Bbx.Tests/Features/Downloads/GetDownloadHandlerTests.csUpdates download handler tests for streaming-to-destination behavior.
tests/Bbx.Tests/Features/Auth/AuthTokenHandlerTests.csUpdates auth token tests for exception-based user errors and return values.
tests/Bbx.Tests/Features/Auth/AuthStatusHandlerTests.csUpdates auth status tests for exception-based failures and API error propagation.
tests/Bbx.Tests/Commands/ConfirmationOutputTests.csEnsures destructive prompts write to stderr (stdout remains script-safe).
tests/Bbx.Tests/Commands/CommandRunnerTests.csUpdates expectations: errors now bubble to the program boundary.
tests/Bbx.Tests/Commands/CommandBindingTests.csVerifies System.CommandLine cancellation token reaches bound handlers.
tests/Bbx.Tests/Auth/FileCredentialStoreTests.csAdds tests for atomic credential writes and strict Unix permissions.
tests/Bbx.Tests/Api/BitbucketClientTests.csAdds redirect/auth-origin and streaming-copy coverage for the API client.
src/Bbx/Program.csCentralizes exception handling for the CLI and configures invocation behavior.
src/Bbx/Features/Workspaces/ViewWorkspace/ViewWorkspaceHandler.csRemoves redundant credential checks; hardens nested JSON handling via TryGetObject.
src/Bbx/Features/Workspaces/ListWorkspaces/ListWorkspacesHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Workspaces/ListWorkspacePermissions/ListWorkspacePermissionsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Workspaces/ListWorkspaceMembers/ListWorkspaceMembersHandler.csUses TryGetObject to avoid null-object pitfalls.
src/Bbx/Features/Users/ViewUser/ViewUserHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ViewSshKey/ViewSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/ListSshKeys/ListSshKeysHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/DeleteSshKey/DeleteSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/SshKeys/AddSshKey/AddSshKeyHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserWorkspacePermissions/ListUserWorkspacePermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserRepositoryPermissions/ListUserRepositoryPermissionsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Users/ListUserEmails/ListUserEmailsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Source/LsSource/LsSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Source/CatSource/CatSourceHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Snippets/ViewSnippet/ViewSnippetHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Snippets/UpdateSnippet/UpdateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetWatch/SnippetWatchHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/SnippetFiles/SnippetFilesHandler.csEscapes per-segment file paths while preserving directories.
src/Bbx/Features/Snippets/SnippetComments/SnippetCommentsHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/ListSnippets/ListSnippetsHandler.csRemoves redundant credential checks and updates handler dependency shape.
src/Bbx/Features/Snippets/DeleteSnippet/DeleteSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Snippets/CreateSnippet/CreateSnippetHandler.csRemoves redundant credential checks.
src/Bbx/Features/Repos/RepoPermissions/RepoPermissionsHandler.csAvoids null nested-object access when shaping permissions output.
src/Bbx/Features/Pipelines/ViewPipeline/ViewPipelineHandler.csHardens steps shaping against non-array/null values.
src/Bbx/Features/Pipelines/ViewDeploymentEnvironment/ViewDeploymentEnvironmentHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/PipelineFormat.csReplaces dynamic shaping with typed records while preserving output shape.
src/Bbx/Features/Pipelines/ListPipelineVariables/ListPipelineVariablesHandler.csSafely reads boolean properties without throwing on null/non-bool.
src/Bbx/Features/Pipelines/ListPipelineSchedules/ListPipelineSchedulesHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Pipelines/ListDeploymentEnvironments/ListDeploymentEnvironmentsHandler.csUses TryGetObject for nullable nested JSON members.
src/Bbx/Features/Downloads/GetDownload/GetDownloadHandler.csSwitches download handling to streaming via a destination stream.
src/Bbx/Features/Common/EndpointPath.csIntroduces shared helper for per-segment path escaping.
src/Bbx/Features/Commits/FileHistory/FileHistoryHandler.csCentralizes safe endpoint path segment escaping.
src/Bbx/Features/Auth/Token/AuthTokenHandler.csReturns token string and throws user errors instead of writing/setting exit code.
src/Bbx/Features/Auth/Status/AuthStatusHandler.csReturns status string and lets API failures propagate to the program boundary.
src/Bbx/Commands/WorkspaceCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/UserCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SrcCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/SnippetCommand.csCentralizes direct command error handling and propagates cancellation.
src/Bbx/Commands/RepoCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/PrCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/PipelineCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/IssueCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/DownloadCommand.csStreams downloads to file/stdout with safer temp-file handling and cancellation support.
src/Bbx/Commands/CommitCommand.csPropagates cancellation and standardizes handler wiring.
src/Bbx/Commands/CommandRunner.csRefactors runner helpers to rely on program-boundary exception handling and adds stream runner.
src/Bbx/Commands/CommandBinding.csAdds AsyncLocal cancellation token propagation into handlers.
src/Bbx/Commands/BranchCommand.csMoves destructive prompts to stderr and propagates cancellation.
src/Bbx/Commands/AuthCommand.csSwitches auth commands to throw user errors and standardizes cancellation propagation.
src/Bbx/Auth/FileCredentialStore.csMakes credential load failures explicit; saves atomically with strict Unix permissions.
src/Bbx/Api/BitbucketClient.csAdds streaming CopyToAsync and tightens auth scoping across redirects/origins.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Bbx/Program.cs Outdated
Comment on lines +69 to +72
exitCode = await parseResult.InvokeAsync(new InvocationConfiguration
{
EnableDefaultExceptionHandler = false,
}, default);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half right, so I checked it against the library rather than the description. Fixed in 9b855c9.

Ctrl+C already reached handlers.InvocationPipeline.InvokeAsync does CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) and gives the action cts.Token, then hands that same source to ProcessTerminationHandler, which registers for SIGINT and SIGTERM via PosixSignalRegistration on .NET 7+. So the token CommandBinding sees is cancelable even when default goes in, and a signal cancels it.

Verified against the built tool, Ctrl+C sent through a real pty mid-request:

baseline, no Ctrl+C: exit=0, 4988 bytes of JSON
Ctrl+C at 0.4s: exit=130, no output

I did not take the Console.CancelKeyPress suggestion. ProcessTerminationHandler only falls back to CancelKeyPress on platforms without PosixSignalRegistration; adding our own subscriber would be a second handler competing with the library's for the same signal.

Two things were wrong. First, a caller driving RunAsync had no way to cancel, since the token was hardcoded. It now takes an optional CancellationToken.

Second, and more visible: cancellation worked but did not exit cleanly. The OperationCanceledException fell through to the catch-all and printed Error: The operation was canceled. with exit 1. Confirmed on the pre-change build:

OLD build, Ctrl+C at 0.4s: exit=1, "Error: The operation was canceled."
NEW build, Ctrl+C at 0.4s: exit=130, no output

A cancelled run is not an error, so it now exits 130, the shell convention for SIGINT, and prints nothing. An HttpClient timeout also surfaces as an OperationCanceledException, but carries a TimeoutException inside, so the filter lets it through to the failure path and it still exits 1.

Tests.Cancelling_the_supplied_token_stops_the_command_before_it_calls_the_api fails on the old code with Expected exitCode to be 130, but found 0 and passes on the new. Bound_handlers_get_a_cancelable_token_even_with_no_token_supplied pins the library behaviour above, so anyone reading this thread and reaching for a signal hook gets a test explaining why not.

That test needed one test-kit fix: FakeHttpMessageHandler ignored the token. HttpClient passes the token straight to its handler without checking it first, so the fake was answering requests a real handler would have refused.

224 tests pass. Also exercised end to end with the packed tool: read-only commands against real repos, and the write and destructive paths (repo create, src write, branch create, pr create, pr decline, branch delete, repo delete) against a throwaway repo, since deleted.

Program.RunAsync hardcoded `default` as the token for InvokeAsync, so a
caller driving RunAsync had no way to stop a command. It now takes an
optional token and passes it through.
Ctrl+C already reached handlers: System.CommandLine links whatever token
it is given to a source of its own and cancels that from
ProcessTerminationHandler. What it did not do was exit cleanly. The
resulting OperationCanceledException fell to the catch-all and printed
"Error: The operation was canceled." with exit 1. A cancelled run is not
an error, so it now exits 130 and says nothing. An HttpClient timeout
also surfaces as an OperationCanceledException but carries a
TimeoutException inside, so it still reports as a failure with exit 1.
FakeHttpMessageHandler now honours the token. HttpClient hands it to the
handler without checking it first, so a fake that ignored it answered
requests a real handler would have refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 67 out of 67 changed files in this pull request and generated no new comments.

@solrevdev
solrevdev merged commit 4706ca7 into masterAug 4, 2026
3 checks passed
@solrevdev
solrevdev deleted the fix/repository-review-findings branch August 4, 2026 08:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@solrevdev