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