fix: Change Account signed out BEFORE the browser opened
Reported 2026-09-27: click avatar -> Change Account -> cancel in the Google chooser -> back to a signed-out app, then "cannot sign-in because the callback url was not released". Two independent defects: 1. ChangeAccountAsync ran `await SignOutYouTubeAsync(); await SignInAsync();`. SignOutYouTubeAsync -> TokenStore.Clear() -> deletes ytLlive.auth, so the working account's token was destroyed before the browser even opened. It is now just `await SignInAsync()`: success overwrites the token via the sessionChanged hook, cancel/failure keeps the current session. The explicit sign-out was never needed. 2. Abandoning the browser gives the app no signal, so the OAuth attempt held localhost:8765 for its full 5-minute timeout and a retry could not bind. A new AuthenticateAsync now cancels and awaits the abandoned attempt first. That takeover is serialised by a SemaphoreSlim: the first version cleared the in-flight marker before awaiting, so a caller arriving in that window saw a free port and raced to bind it -- reproducing the very failure it was written to prevent. My own test caught that; reasoning had "proved" it fine. Also adds an `openBrowser` ctor seam so tests never shell out to a real browser, and corrects the ai.md line that documented "Logout / Change Account -> SignOutYouTubeAsync" as intended behaviour. Test: YouTubeAuthServiceTests.RetryingAfterAnAbandonedAttempt_ReleasesTheCallbackPort Full suite 370/370, 0 warnings. Coverage gap noted in HANDOFF.md: the ordering fix has no automated test because _youtubeAuth is hardcoded in MainViewModel and ChangeAccountAsync is private. The reported cross-instance effect is NOT this cause and stays open -- no code path can sign out a second instance (per-instance TokenStore, local delete, no server-side revoke).
This commit is contained in:
@@ -126,9 +126,21 @@ public partial class MainViewModel
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Change account: sign in to a different YouTube account WITHOUT
|
||||
/// destroying the current session first.
|
||||
/// <para><b>The bug this fixes (2026-09-27).</b> This used to be
|
||||
/// <c>await SignOutYouTubeAsync(); await SignInAsync();</c> — it signed out
|
||||
/// <i>before</i> opening the browser, and <c>SignOutYouTubeAsync</c> calls
|
||||
/// <c>TokenStore.Clear()</c>, which DELETES the saved token file. The creator
|
||||
/// clicked "Change Account", cancelled in the Google account chooser, and came
|
||||
/// back to an app that had already thrown away a working, signed-in account.
|
||||
/// </para>
|
||||
/// <para>No sign-out is needed at all: on success <see cref="SignInAsync"/>
|
||||
/// replaces the session and the <c>sessionChanged</c> hook overwrites the token
|
||||
/// file with the new account. On cancel or failure it returns null and leaves
|
||||
/// the current session untouched — which is the behaviour that was wanted.</para></summary>
|
||||
private async Task ChangeAccountAsync()
|
||||
{
|
||||
await SignOutYouTubeAsync();
|
||||
await SignInAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user