fix(claude-code): send the login link and the code question as one message

This commit is contained in:
EugeneTes
2026-09-25 17:53:14 +00:00
parent fb7be219e5
commit f620b33771
4 changed files with 23 additions and 17 deletions

View File

@@ -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);
}

View File

@@ -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]

View File

@@ -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);
}

View File

@@ -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]