Public Access
Give hosts and terminals their own screen, and the rest of the vault another
Rebuilds the client's shell from an imported design: a titlebar and nav rail it draws itself, real multi-session tabs over the one WebView, a Ctrl+K host search, and a vault screen that merges keys, passwords and pinned host keys into one table. Hosts left the vault column for their own screen beside the terminal, which is what the design asks for and turned out to be the better split anyway. Two screens the design shows have nothing behind them yet — file transfer and teams — and say so plainly rather than rendering invented data; every other gap between the design and this build is recorded in docs/design-import-gaps.md.
This commit is contained in:
@@ -117,7 +117,95 @@ public sealed class TerminalWorkspaceTests
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Disposal is the one path that does close sessions, because it is process shutdown. Asserted so
|
||||
/// <para>
|
||||
/// Per-session liveness, which the tab strip's status dot is. The aggregate count answers "may I walk
|
||||
/// away"; this answers "is <em>this</em> tab still connected", and a tab that went on claiming a shell
|
||||
/// it no longer has would be the same lie one row down.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The unknown-id case is asserted because it is what a stale tab asks. A session id this workspace
|
||||
/// never issued, or has already closed, is not live — it must not throw and must not be optimistic.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task LivenessIsAnsweredPerSession()
|
||||
{
|
||||
var connections = new FakeConnectionFactory();
|
||||
|
||||
await using var workspace = CreateWorkspace(connections);
|
||||
|
||||
var first = await workspace.OpenSessionAsync(
|
||||
Request(), TerminalSize.Default, TestContext.Current.CancellationToken);
|
||||
var second = await workspace.OpenSessionAsync(
|
||||
Request(), TerminalSize.Default, TestContext.Current.CancellationToken);
|
||||
|
||||
workspace.IsSessionLive(first).ShouldBeTrue();
|
||||
workspace.IsSessionLive(second).ShouldBeTrue();
|
||||
workspace.IsSessionLive(9999).ShouldBeFalse("this workspace never issued that id");
|
||||
|
||||
await workspace.CloseSessionAsync(first);
|
||||
|
||||
workspace.IsSessionLive(first).ShouldBeFalse();
|
||||
workspace.IsSessionLive(second).ShouldBeTrue("closing one tab must not disturb another");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The event the tab strip listens to, so a dot can go out the moment a shell exits rather than at the
|
||||
/// next thing that happens to repaint. Raised only when the session ended on its own: a tab the user
|
||||
/// closed has a caller who already knows, and telling it would turn one close into two.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ASessionEndingOnItsOwnIsAnnounced()
|
||||
{
|
||||
// A shell with no output to give: its first read returns 0, which is a remote closing the channel,
|
||||
// so the pump finishes with nobody asking it to.
|
||||
var connections = new FakeConnectionFactory(bytesPerShell: 0);
|
||||
|
||||
await using var workspace = CreateWorkspace(connections);
|
||||
|
||||
var ended = new List<uint>();
|
||||
workspace.SessionEnded += (_, e) =>
|
||||
{
|
||||
lock (ended)
|
||||
{
|
||||
ended.Add(e.SessionId);
|
||||
}
|
||||
};
|
||||
|
||||
var sessionId = await workspace.OpenSessionAsync(
|
||||
Request(), TerminalSize.Default, TestContext.Current.CancellationToken);
|
||||
|
||||
await WaitUntilAsync(() =>
|
||||
{
|
||||
lock (ended)
|
||||
{
|
||||
return ended.Contains(sessionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ASessionEndingOnItsOwnIsAnnounced" />
|
||||
[Fact]
|
||||
public async Task ClosingASessionIsNotAnnouncedBack()
|
||||
{
|
||||
var connections = new FakeConnectionFactory();
|
||||
|
||||
await using var workspace = CreateWorkspace(connections);
|
||||
|
||||
var announcements = 0;
|
||||
workspace.SessionEnded += (_, _) => Interlocked.Increment(ref announcements);
|
||||
|
||||
var sessionId = await workspace.OpenSessionAsync(
|
||||
Request(), TerminalSize.Default, TestContext.Current.CancellationToken);
|
||||
|
||||
await workspace.CloseSessionAsync(sessionId);
|
||||
|
||||
Volatile.Read(ref announcements)
|
||||
.ShouldBe(0, "a close the caller asked for is not news to report back to it");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Disposal is the other path that closes sessions, because it is process shutdown. Asserted so
|
||||
/// that the SSH connections are known to be released rather than assumed to be.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user