Public Access
Merge branch 'claude/distracted-ritchie-53fc70'
Bounds the renderer wait, so a WebView2 that never initialises reports itself instead of hanging Connect with the busy flag stuck. Conflict resolution, all of it in the App test suite, which main had changed under the branch when sleepy-chebyshev landed: - The workspace fixture keeps main's fake SSH factory and its FakeRenderer-aware page, and takes the branch's RendererTimeout on top. One second rather than the branch's 250 ms, because the timeout now also bounds FakeRenderer's own wait for the attach it just made. - FakeRenderer arrived on main after the branch was cut and still called the no-argument WaitForRendererAsync. Both sides merged cleanly and left the build broken; it now passes its own token. - ConnectingWithNoRenderer's remark claimed the suite never starts the workspace and never attaches a renderer. Both are false here, so it now says what is true of the test: it is the one connect test that attaches no renderer.
This commit is contained in:
@@ -66,7 +66,7 @@ internal sealed partial class FakeRenderer : IAsyncDisposable
|
||||
|
||||
var renderer = new FakeRenderer(attached);
|
||||
|
||||
await workspace.WaitForRendererAsync().ConfigureAwait(false);
|
||||
await workspace.WaitForRendererAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,12 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
//
|
||||
// The page carries the same two placeholders the real one does, because FakeRenderer attaches by
|
||||
// reading them back out of the served page rather than by being handed the token.
|
||||
//
|
||||
// The renderer timeout is cut right down for the same reason: the tests that want a renderer attach
|
||||
// one themselves, so a connect in a test that does not would wait the shipped fifteen seconds out
|
||||
// in full, and that is fifteen seconds of a suite sitting still. A second rather than milliseconds
|
||||
// because this bounds FakeRenderer's own wait too — an in-process loopback handshake that has
|
||||
// already returned, so the margin is enormous, but not one worth making a loaded machine race for.
|
||||
workspace = new TerminalWorkspace(
|
||||
new InMemoryTerminalAssetProvider(
|
||||
new Dictionary<string, TerminalAsset>(StringComparer.Ordinal)
|
||||
@@ -72,7 +78,8 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
+ $"data-socket=\"{TerminalDataPlane.SocketUrlPlaceholder}\"></div>")),
|
||||
}),
|
||||
ssh,
|
||||
TimeProvider.System);
|
||||
TimeProvider.System,
|
||||
new TerminalWorkspaceOptions { RendererTimeout = TimeSpan.FromSeconds(1) });
|
||||
|
||||
// Started, as the application does immediately after composing it. Without the accept loop the
|
||||
// page is never served, so nothing could attach a renderer.
|
||||
@@ -597,6 +604,30 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
server.PushCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The one connect test that deliberately attaches no <see cref="FakeRenderer"/>: the listener is up
|
||||
/// and nothing ever connects to it, which from the view model's side is indistinguishable from a
|
||||
/// WebView2 that failed to initialise on a user's machine. The interesting assertion is the second
|
||||
/// one: while the wait was unbounded this hung with the busy flag set, so the window stayed disabled
|
||||
/// and said "Connecting…" for the rest of the session.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ConnectingWithNoRenderer_ExplainsItselfAndReleasesTheWindow()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
var vault = shell.Vault!;
|
||||
|
||||
await AddHostAsync(vault, "prod-db");
|
||||
vault.SelectedHost = vault.Hosts[0];
|
||||
|
||||
await vault.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
// Naming the runtime is the whole point: a bare "The operation has timed out" sends someone
|
||||
// looking at their network or their host.
|
||||
vault.Status.ShouldContain("WebView2");
|
||||
vault.IsBusy.ShouldBeFalse("a connect that gave up must not leave the window disabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LockingForgetsTheVault()
|
||||
{
|
||||
|
||||
@@ -57,7 +57,7 @@ public sealed class TerminalEndToEndTests(SshServerFixture fixture)
|
||||
var token = await ReadTokenAsync(workspace.PageUrl);
|
||||
|
||||
using var renderer = await AttachAsync(workspace.PageUrl, token);
|
||||
await workspace.WaitForRendererAsync();
|
||||
await workspace.WaitForRendererAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
var sessionId = await OpenTrustedSessionAsync(workspace, knownHosts);
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Text;
|
||||
using DodoSSH.Client.Ssh;
|
||||
using NSubstitute;
|
||||
|
||||
namespace DodoSSH.Client.Terminal.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The renderer gate: the one place the workspace waits on something outside the process.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The gate itself is not in question. <see cref="TerminalDataPlane.SendAsync"/> drops frames when nothing
|
||||
/// is attached, so a session opened before the renderer arrives loses its <c>SessionOpened</c> frame and
|
||||
/// streams output at a terminal that was never created — and that it opens when a renderer does attach is
|
||||
/// covered by <see cref="TerminalDataPlaneTests.TheRenderer_Attaches"/>. What is worth a test here is the
|
||||
/// half that used to be missing: waiting for a renderer that never arrives has to end.
|
||||
/// </remarks>
|
||||
public sealed class TerminalWorkspaceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task WaitingForARendererThatNeverAttaches_GivesUp()
|
||||
{
|
||||
// The shipped failure this stands in for is a WebView2 that never initialises — no Evergreen
|
||||
// runtime, an install blocked by policy, an AppContainer that cannot reach loopback. From this
|
||||
// side they are identical and all look like the listener being up with nothing ever connecting
|
||||
// to it. Before the wait was bounded this test would have hung instead of failing.
|
||||
await using var workspace = CreateWorkspace(TimeSpan.FromMilliseconds(250));
|
||||
workspace.Start();
|
||||
|
||||
await Should.ThrowAsync<TimeoutException>(async () =>
|
||||
await workspace.WaitForRendererAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WaitingForARenderer_ObeysItsCancellationToken()
|
||||
{
|
||||
// The timeout is the backstop; the caller's token is what makes a Connect the user gave up on
|
||||
// return at once rather than sitting out the rest of the wait. The timeout here is long enough
|
||||
// that only cancellation can end this.
|
||||
await using var workspace = CreateWorkspace(TimeSpan.FromMinutes(5));
|
||||
workspace.Start();
|
||||
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
var wait = workspace.WaitForRendererAsync(cancellation.Token);
|
||||
|
||||
await cancellation.CancelAsync();
|
||||
|
||||
await Should.ThrowAsync<OperationCanceledException>(async () => await wait);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The connection factory is never reached: every test here stops at the gate, and reaching a real
|
||||
/// host would make this a network test.
|
||||
/// </remarks>
|
||||
private static TerminalWorkspace CreateWorkspace(TimeSpan rendererTimeout) =>
|
||||
new(
|
||||
new InMemoryTerminalAssetProvider(
|
||||
new Dictionary<string, TerminalAsset>(StringComparer.Ordinal)
|
||||
{
|
||||
[TerminalDataPlane.PagePath] = new(
|
||||
"text/html; charset=utf-8",
|
||||
Encoding.UTF8.GetBytes("<html><body></body></html>")),
|
||||
}),
|
||||
Substitute.For<ISshConnectionFactory>(),
|
||||
TimeProvider.System,
|
||||
new TerminalWorkspaceOptions { RendererTimeout = rendererTimeout });
|
||||
}
|
||||
Reference in New Issue
Block a user