-
Notifications
You must be signed in to change notification settings - Fork 18
fix(copilot): restore OAuth exchange + surface LLM failure detail #1159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Aaronontheweb
merged 3 commits into
netclaw-dev:dev
from
Aaronontheweb:claude-wt-github-copilot-oauth-handoff
May 23, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
72e8079
fix(providers): restore Copilot OAuth by reverting to allowlisted OAu…
Aaronontheweb 2ff7723
fix(sessions): surface HTTP / transport failure detail from LlmFailur…
Aaronontheweb 1aa8f60
Merge branch 'dev' into claude-wt-github-copilot-oauth-handoff
Aaronontheweb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
148 changes: 148 additions & 0 deletions
148
src/Netclaw.Actors.Tests/Sessions/LlmFailureClassifierTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| // ----------------------------------------------------------------------- | ||
| // <copyright file="LlmFailureClassifierTests.cs" company="Petabridge, LLC"> | ||
| // Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com> | ||
| // </copyright> | ||
| // ----------------------------------------------------------------------- | ||
| using System.Net; | ||
| using System.Net.Http; | ||
| using Netclaw.Actors.Sessions; | ||
| using Netclaw.Configuration; | ||
| using Xunit; | ||
|
|
||
| namespace Netclaw.Actors.Tests.Sessions; | ||
|
|
||
| public class LlmFailureClassifierTests | ||
| { | ||
| private static readonly ModelCapabilities Model = new() | ||
| { | ||
| ModelId = "test-model", | ||
| ContextWindowTokens = 8000, | ||
| }; | ||
|
|
||
| [Fact] | ||
| public void NullCause_ReturnsGenericMessage() | ||
| { | ||
| var message = LlmFailureClassifier.ExtractUserMessage(null, Model); | ||
|
|
||
| Assert.Equal("I encountered an error processing your message. Please try again.", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProviderException_UsesProviderUserMessage() | ||
| { | ||
| // Provider-curated messages bypass our heuristics — the provider | ||
| // already decided what's safe to surface. | ||
| var ex = new ProviderException("Custom provider message", "internal detail", statusCode: 500); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(ex, Model); | ||
|
|
||
| Assert.Equal("Custom provider message", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void TimeoutException_GetsTimeoutMessage() | ||
| { | ||
| var message = LlmFailureClassifier.ExtractUserMessage(new TimeoutException("idle"), Model); | ||
|
|
||
| Assert.Contains("timed out", message, StringComparison.OrdinalIgnoreCase); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(HttpStatusCode.Unauthorized, "401")] | ||
| [InlineData(HttpStatusCode.Forbidden, "403")] | ||
| public void HttpRequestException_AuthStatus_PromptsReauth(HttpStatusCode status, string statusText) | ||
| { | ||
| var ex = new HttpRequestException("auth rejected", inner: null, statusCode: status); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(ex, Model); | ||
|
|
||
| Assert.Contains(statusText, message); | ||
| Assert.Contains("netclaw provider add", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HttpRequestException_429_IsNamedAsRateLimit() | ||
| { | ||
| var ex = new HttpRequestException("too many", inner: null, statusCode: HttpStatusCode.TooManyRequests); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(ex, Model); | ||
|
|
||
| Assert.Contains("rate-limited", message, StringComparison.OrdinalIgnoreCase); | ||
| Assert.Contains("429", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HttpRequestException_5xx_IsNamedAsServerError() | ||
| { | ||
| var ex = new HttpRequestException("upstream offline", inner: null, | ||
| statusCode: HttpStatusCode.BadGateway); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(ex, Model); | ||
|
|
||
| Assert.Contains("server error", message, StringComparison.OrdinalIgnoreCase); | ||
| Assert.Contains("502", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HttpRequestException_NullStatus_SurfacesTransportDetail() | ||
| { | ||
| // This is the case the user actually hit — OllamaSharp threw | ||
| // HttpRequestException("Connection refused (localhost:11434)") with | ||
| // no StatusCode, and the old classifier swallowed it. | ||
| var ex = new HttpRequestException("Connection refused (localhost:11434)"); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(ex, Model); | ||
|
|
||
| Assert.Contains("transport error", message, StringComparison.OrdinalIgnoreCase); | ||
| Assert.Contains("Connection refused (localhost:11434)", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void HttpRequestException_NestedInInvocationException_IsStillUnwrapped() | ||
| { | ||
| // Akka and task-based pipelines often wrap the real cause in | ||
| // outer exceptions; the classifier must walk the chain. | ||
| var inner = new HttpRequestException("Connection refused (host:1234)"); | ||
| var wrapped = new InvalidOperationException("LLM call failed", inner); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(wrapped, Model); | ||
|
|
||
| Assert.Contains("transport error", message, StringComparison.OrdinalIgnoreCase); | ||
| Assert.Contains("Connection refused (host:1234)", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void UnknownException_SurfacesTypeAndTruncatedMessage() | ||
| { | ||
| var ex = new InvalidOperationException("something specific went wrong"); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(ex, Model); | ||
|
|
||
| Assert.Contains("InvalidOperationException", message); | ||
| Assert.Contains("something specific went wrong", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void UnknownException_LongMessage_GetsTruncated() | ||
| { | ||
| var longMessage = new string('x', 1000); | ||
| var ex = new InvalidOperationException(longMessage); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(ex, Model); | ||
|
|
||
| Assert.True(message.Length < 500, | ||
| $"forwarded message length should be capped; got {message.Length}"); | ||
| Assert.EndsWith("…", message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ContextOverflow_NamesTheModelAndContextWindow() | ||
| { | ||
| var ex = new InvalidOperationException("prompt is too long for the context"); | ||
|
|
||
| var message = LlmFailureClassifier.ExtractUserMessage(ex, Model); | ||
|
|
||
| Assert.Contains("test-model", message); | ||
| Assert.Contains("8000", message); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is extremely annoying and hopefully we can use our own github app again in the future