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:
+34
-2
@@ -1,8 +1,40 @@
|
||||
# HANDOFF — current state
|
||||
|
||||
**Branch:** `main` (pre-1.0, no feature branches — creator ruling 2026-08-24).
|
||||
**Last pushed:** `b2036a3`. This unit (reconciling the pricing ruling + verifying the Store
|
||||
revenue share) is **docs-only** — no code touched, no build, no tests run.
|
||||
**Last pushed:** `e7cded6` (pricing/Store fee docs). **Local, unpushed:** `536676f` (recording
|
||||
filename ms) and the Change Account fix in this unit — ask before pushing.
|
||||
|
||||
## 🚨 FIXED 2026-09-27 — "Change Account" no longer signs you out before the browser opens
|
||||
|
||||
**Reported as:** *"I clicked on my signed-in account pic, then clicked sign-in to another account. It
|
||||
opened the browser page to login under a different account and I canceled the request. But when
|
||||
returned to the app, I see logged out — and should not have happened! Cannot sign-in because the
|
||||
callback url was not released."* Also reported as hitting **both** running instances.
|
||||
|
||||
**Two independent defects, and the second one was masking a race:**
|
||||
|
||||
1. **The sign-out ran first.** `ChangeAccountAsync` was `await SignOutYouTubeAsync(); await
|
||||
SignInAsync();`. `SignOutYouTubeAsync` → `TokenStore.Clear()` → **deletes** `ytLlive.auth`. So the
|
||||
working account's token was destroyed *before the browser even opened*; cancelling the chooser left
|
||||
nothing. Now `ChangeAccountAsync` is just `await SignInAsync()` — success overwrites the token via
|
||||
the `sessionChanged` hook, cancel/failure keeps the current session. No sign-out is needed at all.
|
||||
2. **The retry couldn't rebind the callback port.** Abandoning the browser gives the app no signal, so
|
||||
the attempt held `localhost:8765` for its full 5-minute timeout. A new `AuthenticateAsync` now
|
||||
cancels and awaits the abandoned attempt before binding, **serialised by a `SemaphoreSlim`**
|
||||
(`_authGate`) — without the gate, the takeover window let a third caller see a free port and race
|
||||
to bind it, i.e. the takeover reproduced the original failure. My own test caught this.
|
||||
|
||||
**The cross-instance claim was NOT the cause** and is still open as a *separate* question: no code path
|
||||
can sign out a second instance (`TokenStore` is per-`InstanceProfile.DataRoot`, `Clear()` is a local
|
||||
file delete with no server-side revoke, and no second instance had a token at inspection time). Verify
|
||||
both processes carried distinct `YTLIVE_INSTANCE` values and ran Debug builds before chasing it.
|
||||
|
||||
Test: `YouTubeAuthServiceTests.RetryingAfterAnAbandonedAttempt_ReleasesTheCallbackPort`. Full suite
|
||||
**370/370**, 0 warnings (build `ytLive.Tests.csproj`, not just the app project, or VSTest runs a stale
|
||||
test DLL). Added an `openBrowser` ctor seam to `YouTubeAuthService` so tests never launch a real
|
||||
browser. **Coverage gap:** the *ordering* fix (defect 1) has no automated test — `_youtubeAuth` is
|
||||
hardcoded in `MainViewModel` and `ChangeAccountAsync` is private, so a real test would shell out to
|
||||
the browser. Wiring an auth seam into `MainViewModel` is a deliberate follow-up, not a bug-fix extra.
|
||||
|
||||
## ✅ DECIDED 2026-09-27 — pricing: `$10/mo` subscription, or `$400` lifetime, **no annual**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user