forked from admin/VolcanicAgenticCore
fix(claude-code): send the login link and the code question as one message
This commit is contained in:
@@ -2,21 +2,21 @@ using Volcanic.AgenticCore.Abstractions.Ui;
|
||||
|
||||
namespace Volcanic.AgenticCore.Extensions.ClaudeCode;
|
||||
|
||||
/// <summary><see cref="ILoginPrompt"/> over a host dialog surface: the link as a notification, the code as a free-text answer.</summary>
|
||||
/// <summary>
|
||||
/// <see cref="ILoginPrompt"/> over a host dialog surface: the instructions and the link are the title of the free-text
|
||||
/// question for the code, one message, so the link can never arrive after (or without) the question.
|
||||
/// </summary>
|
||||
public sealed class UiLoginPrompt(IUserInterface ui) : ILoginPrompt
|
||||
{
|
||||
public const string CodeQuestion = "Paste the Claude login code";
|
||||
|
||||
private readonly IUserInterface _ui = ui ?? throw new ArgumentNullException(nameof(ui));
|
||||
|
||||
public async Task<string?> AskForCodeAsync(string url, int attempt, CancellationToken cancellationToken)
|
||||
{
|
||||
_ui.Notify("Claude Code needs you to sign in again.\n" +
|
||||
$"1. Open: {url}\n" +
|
||||
"2. Sign in with the Claude subscription account.\n" +
|
||||
"3. Paste the code shown on the page as a reply.");
|
||||
return await _ui.InputAsync(CodeQuestion, "code", null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<string?> AskForCodeAsync(string url, int attempt, CancellationToken cancellationToken) =>
|
||||
await _ui.InputAsync("Claude Code needs you to sign in again.\n" +
|
||||
$"1. Open: {url}\n" +
|
||||
"2. Sign in with the Claude subscription account.\n" +
|
||||
$"3. {CodeQuestion} shown on the page as a reply.", "code", null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
public void Report(string message) => _ui.Notify(message, NotifyKind.Warning);
|
||||
}
|
||||
|
||||
@@ -266,9 +266,10 @@ public class ClaudeCodeApiTests : IDisposable
|
||||
Assert.Equal(StopReason.ToolUse, Assert.IsType<AssistantMessageEvent.Done>(events[^1]).Reason);
|
||||
Assert.Equal(2, Runs());
|
||||
Assert.Contains(notes, n => n.Contains("waiting for the bot owner"));
|
||||
Assert.Contains(owner.Notes, n => n.Message.Contains("oauth/authorize"));
|
||||
Assert.Contains(owner.Notes, n => n.Message.Contains("login complete"));
|
||||
Assert.Equal([UiLoginPrompt.CodeQuestion], owner.Questions);
|
||||
var question = Assert.Single(owner.Questions);
|
||||
Assert.Contains("oauth/authorize", question);
|
||||
Assert.Contains(UiLoginPrompt.CodeQuestion, question);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -27,13 +27,16 @@ internal sealed class RecordingUi(params string?[] inputs) : IUserInterface
|
||||
public class UiLoginPromptTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Sends_the_link_then_asks_for_the_code()
|
||||
public async Task Asks_for_the_code_with_the_link_in_one_question()
|
||||
{
|
||||
var ui = new RecordingUi("abc");
|
||||
var code = await new UiLoginPrompt(ui).AskForCodeAsync("https://x/oauth/authorize?s=1", 1, CancellationToken.None);
|
||||
Assert.Equal("abc", code);
|
||||
Assert.Contains("https://x/oauth/authorize?s=1", ui.Notes.Single().Message);
|
||||
Assert.Equal([UiLoginPrompt.CodeQuestion], ui.Questions);
|
||||
// One message: a separate link notification could overtake (or trail) the question on an async surface.
|
||||
var question = Assert.Single(ui.Questions);
|
||||
Assert.Contains("https://x/oauth/authorize?s=1", question);
|
||||
Assert.Contains(UiLoginPrompt.CodeQuestion, question);
|
||||
Assert.Empty(ui.Notes);
|
||||
Assert.Equal("Paste the Claude login code", UiLoginPrompt.CodeQuestion);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Telegram.Bot.Types.ReplyMarkups;
|
||||
using Volcanic.AgenticCore.Extensions.ClaudeCode;
|
||||
using Volcanic.AgenticCore.Telegram.Chats;
|
||||
using Volcanic.AgenticCore.Telegram.Tests.Support;
|
||||
@@ -25,10 +26,11 @@ public sealed class OwnerLoginTests
|
||||
Assert.Equal("pasted-code", await ask.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
Assert.Empty(host.Faux.Requests);
|
||||
|
||||
await host.SettleAsync(); // the link notification goes through the renderer's async queue
|
||||
Assert.Contains(host.Bot.Sent, s => s.ChatId.Identifier == Updates.AllowedUser
|
||||
// The link and the code question are one message, so they can never arrive out of order.
|
||||
var prompt = Assert.Single(host.Bot.Sent, s => s.ChatId.Identifier == Updates.AllowedUser
|
||||
&& s.Text.Contains("oauth/authorize?state=t", StringComparison.Ordinal));
|
||||
Assert.Contains(host.Bot.Sent, s => s.ChatId.Identifier == Updates.AllowedUser && s.Text == UiLoginPrompt.CodeQuestion);
|
||||
Assert.Contains(UiLoginPrompt.CodeQuestion, prompt.Text);
|
||||
Assert.IsType<ForceReplyMarkup>(prompt.ReplyMarkup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user