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:
2026-09-27 16:02:05 -07:00
parent 536676f11a
commit 44344c9327
6 changed files with 200 additions and 13 deletions
+13 -1
View File
@@ -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();
}
}