namespace DodoSSH.Client.Terminal.Tests; /// /// The renderer page's half of staying attached — terminal.js's connect() and what drives it. /// /// /// /// The host's half is , and the two are one mechanism: a socket that /// drops is ordinary here, and the page coming back for another is what makes it ordinary. What these /// tests protect is the property that failure of this mechanism has no other symptom — a terminal whose /// page has given up looks exactly like a terminal whose remote has gone quiet, except for a line of text /// nobody reads twice. /// /// /// Four of these were written against a page that failed them — the stale close, the handshake that never /// finishes, and the two wake-ups — and the rest describe behaviour that was already right and is easy to /// break while fixing those. The two that assert a wake-up does *nothing* pass against either version, /// which is the point of them: they are what stops the cure being worse, and they can only ever fail /// against a future change. See for how the real file is loaded and for what /// this cannot reach. /// /// public sealed class RendererReconnectionTests { [Fact] public void ThePage_ConnectsWhenItLoads() { var page = RendererPage.Load(); page.Attempts.ShouldBe(1); } [Fact] public void ADroppedSocket_IsRetriedAndTheBannerClears() { var page = RendererPage.Load(); page.Do("accept(0)"); page.Banner.ShouldBe(""); page.Do("drop(0)"); page.Banner.ShouldStartWith("Reconnecting"); page.Do("advance(1000)"); page.Attempts.ShouldBe(2); page.Do("accept(1)"); page.Banner.ShouldBe(""); } /// /// The wait grows within one outage and goes back to a second once a socket has actually opened, so /// that the next outage is not paid for at the previous one's rate. /// [Fact] public void TheWait_GrowsWithinAnOutageAndResetsAfterIt() { var page = RendererPage.Load(); page.Do("fail(0); advance(1000)"); page.Attempts.ShouldBe(2); page.Do("fail(1); advance(1999)"); page.Attempts.ShouldBe(2); page.Do("advance(1)"); page.Attempts.ShouldBe(3); page.Do("accept(2); drop(2); advance(1000)"); page.Attempts.ShouldBe(4); } /// /// A close for a socket the page has already replaced must not start a reconnect. /// /// /// The loop this forbids costs nothing to enter and never leaves: the host aborts the displaced socket /// on every takeover — see TerminalDataPlane.UpgradeAsync — so a stale close that schedules a /// retry displaces the socket that has just succeeded, whose own close schedules the next. The visible /// end of it is a terminal that reconnects every second forever with the banner up for most of it. /// [Fact] public void AStaleClose_DoesNotDisplaceTheSocketThatSucceeded() { var page = RendererPage.Load(); page.Do("accept(0); drop(0); advance(1000)"); page.Do("accept(1)"); page.Attempts.ShouldBe(2); // The first socket's end, arriving after the page has moved on. page.Do("deliverLateClose(0)"); page.Do("advance(60000)"); page.Attempts.ShouldBe(2); page.Banner.ShouldBe(""); } /// /// An attempt that never finishes its handshake is given up on rather than waited on forever. /// /// /// Every other retry in the page is scheduled by a close or an error, so a socket that reports neither /// — which is what a renderer suspended mid-handshake leaves behind — used to schedule nothing at all. /// The page then held a banner saying it was reconnecting with no timer pending and no socket coming, /// for the rest of its life. /// [Fact] public void AHandshakeThatNeverFinishes_IsAbandonedAndRetried() { var page = RendererPage.Load(); // Nothing whatever from the first attempt: no open, no error, no close. page.Do("advance(5000)"); page.Attempts.ShouldBe(1); page.Do("advance(1000)"); page.Attempts.ShouldBe(2); page.IsClosed(0).ShouldBeTrue(); page.Do("accept(1)"); page.Banner.ShouldBe(""); } /// /// Coming back to the page reconnects it, without waiting for a timer that may not be running. /// /// /// The case the whole wake-up path exists for, and the one a test can only approximate: the harness's /// clock stands still here because a real hidden page's clock is throttled rather than stopped, and /// standing still is the honest worst case of that. What is being asserted is that the page does not /// need the clock at all to notice it is back. /// [Fact] public void BecomingVisibleAgain_ReconnectsWithoutTheTimer() { var page = RendererPage.Load(); page.Do("accept(0); becomeHidden(); drop(0)"); page.Attempts.ShouldBe(1); page.Do("becomeVisible()"); page.Attempts.ShouldBe(2); // And the timer that was pending when the page woke must not open a third socket on top of the // one that just succeeded — which would be the takeover loop, entered from the other side. page.Do("accept(1); advance(60000)"); page.Attempts.ShouldBe(2); page.Banner.ShouldBe(""); } [Fact] public void TakingTheKeyboardBack_AlsoReconnects() { var page = RendererPage.Load(); page.Do("accept(0); drop(0); takeFocus()"); page.Attempts.ShouldBe(2); } /// /// The wake-ups fire on gestures as ordinary as clicking the window, so the check they make has to be /// the thing that keeps them cheap rather than the frequency. A page whose socket is up must treat all /// of them as nothing at all — anything else would be the takeover loop with a person's mouse driving it. /// [Fact] public void WakingUpOverAHealthySocket_DoesNothing() { var page = RendererPage.Load(); page.Do("accept(0)"); page.Do("takeFocus(); becomeVisible(); comeOnline(); becomeHidden(); becomeVisible()"); page.Attempts.ShouldBe(1); page.Banner.ShouldBe(""); } /// /// An attempt already in flight is left to finish or to time out. Restarting it on every wake-up would /// mean a page being clicked during a slow handshake never completing one. /// [Fact] public void WakingUpWhileConnecting_LeavesTheAttemptAlone() { var page = RendererPage.Load(); page.Do("takeFocus(); becomeVisible(); comeOnline()"); page.Attempts.ShouldBe(1); } }