Public Access
Desktop: show the right vault's people, leave settings on lock, say when an import fell short, and stop host names being cut to four letters
- Choosing a vault while the screen was busy went through a gate that did nothing, so the members
panel kept showing the previous vault's people under the new name. The read now starts regardless;
the selection generation discards answers for an earlier choice.
- Locking or signing out from Settings left the window in settings mode under the lock card, with no
title bar to drag, minimise or close, and unlock returned to Settings. Leaving the unlocked state
leaves Settings and closes the members panel; Escape closes that panel before leaving Settings.
- Both importers reported success when the keychain was busy and skipped the batch, or failed partway.
ImportOutcome now carries refused hosts and why a batch stopped, and the screens say so.
- Host cards: the sync badge took 70 of 214 pixels from the name ("bastion" drew as "bast…", two k8s
workers both as "k8s…"); it now sits on the address line. Tag chips wrap and trim inside the card.
Keychain names trim inside their column instead of printing over TYPE, and the Security page's
caption wraps inside its card.
This commit is contained in:
1 parent
0e8fe4fc80
commit
a4b81da597
11 files changed
+314
-68
No files matched your search
@@ -1871,6 +1871,31 @@ public sealed partial class ShellFlowTests : IAsyncLifetime
|
||||
/// connect and restored afterwards — and the dial itself goes through the ordinary connect path, so the
|
||||
/// host key gate and the log entry are the ones every other connection gets.
|
||||
/// </remarks>
|
||||
/// <remarks>
|
||||
/// A sync pass reloads the vault, which empties its host list and fills it again — and the picker used to
|
||||
/// re-find its selection from the empty moment, landing on the first host every minute. CONNECT then
|
||||
/// dialled that host with the password typed for the one chosen.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheFilesHostPicker_KeepsTheChosenHostAndItsPasswordAcrossAVaultReload()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
await AddHostAsync(vault, "staging-db");
|
||||
|
||||
shell.Transfers.Attach(vault, knownHosts);
|
||||
|
||||
var chosen = shell.Transfers.Hosts.Single(row => row.Host.Label == "staging-db");
|
||||
shell.Transfers.SelectedHost = chosen;
|
||||
shell.Transfers.TypedPassword = "typed for staging";
|
||||
|
||||
shell.Transfers.Hosts[0].ShouldNotBe(chosen, "otherwise falling back to the first proves nothing");
|
||||
|
||||
await vault.LoadAsync(Token);
|
||||
|
||||
shell.Transfers.SelectedHost.ShouldNotBeNull().EntityId.ShouldBe(chosen.EntityId);
|
||||
shell.Transfers.TypedPassword.ShouldBe("typed for staging");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReconnectingALostFileSession_DialsAgainAndGoesBackToTheDirectoryItWasShowing()
|
||||
{
|
||||
@@ -5705,6 +5730,16 @@ public sealed partial class ShellFlowTests : IAsyncLifetime
|
||||
.Single(row => string.Equals(row.Alias, "web-01", StringComparison.Ordinal))
|
||||
.IsSelected.ShouldBeTrue();
|
||||
|
||||
// A keychain busy with something else — a sync pass, a save — used to skip the whole batch and let this
|
||||
// screen say "Imported 0 host(s)" as though that were the result.
|
||||
vault.IsBusy = true;
|
||||
|
||||
await import.ImportCommand.ExecuteAsync(null);
|
||||
|
||||
vault.IsBusy = false;
|
||||
vault.Hosts.Count.ShouldBe(1);
|
||||
import.Status.ShouldContain("Nothing was imported");
|
||||
|
||||
await import.ImportCommand.ExecuteAsync(null);
|
||||
|
||||
var imported = vault.Hosts.Single(row => string.Equals(row.Label, "web-01", StringComparison.Ordinal));
|
||||
@@ -10628,6 +10663,35 @@ public sealed partial class ShellFlowTests : IAsyncLifetime
|
||||
shell.Screen.ShouldBe(ShellScreen.Keychain);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// LOCK NOW is on the Preferences page. Settings mode hides the main title bar and draws its own, which
|
||||
/// the lock card then covers — so a lock that stayed in settings mode left a window with nothing to drag,
|
||||
/// minimise or close, and an unlock that came back to Settings rather than to Hosts.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task LockingFromSettings_LeavesSettingsModeAndUnlockingLandsOnHosts()
|
||||
{
|
||||
await ReadyToConnectAsync();
|
||||
shell.ShowScreenCommand.Execute(ShellScreen.Keychain);
|
||||
|
||||
shell.EnterSettingsCommand.Execute(SettingsPage.Vaults);
|
||||
await shell.Vaults.LoadAsync(Token);
|
||||
shell.Vaults.OpenMembersPanelCommand.Execute(shell.Vaults.Vaults[0]);
|
||||
shell.Vaults.IsMembersPanelOpen.ShouldBeTrue("otherwise its closing proves nothing");
|
||||
|
||||
await shell.LockCommand.ExecuteAsync(null);
|
||||
|
||||
shell.State.ShouldBe(ShellState.Locked);
|
||||
shell.IsSettingsMode.ShouldBeFalse("the lock card needs the main title bar above it");
|
||||
shell.Vaults.IsMembersPanelOpen.ShouldBeFalse();
|
||||
|
||||
shell.Passphrase = Passphrase;
|
||||
await shell.UnlockCommand.ExecuteAsync(null);
|
||||
|
||||
shell.IsSettingsMode.ShouldBeFalse();
|
||||
shell.Screen.ShouldBe(ShellScreen.Hosts);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Settings mode collapses the terminal the same way any other page does — <see cref="ShellSurface.Page"/>
|
||||
/// and <see cref="ShellSurface.Terminal"/> are exclusive by construction — and "Back to application" has
|
||||
|
||||
@@ -2177,10 +2177,70 @@ public sealed class VaultSharingTests : IAsyncLifetime
|
||||
vaults.Members.ShouldHaveSingleItem().Role.ShouldBe("OWNER");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A vault chosen while anything else on the screen was running — a reload, a share — went through a busy
|
||||
/// gate that answered by doing nothing, so its people were never read and the panel kept showing the
|
||||
/// previous vault's under the new one's name. SHARE and REVOKE then acted on that mixture.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AVaultChosenWhileAReloadIsRunning_ShowsItsOwnPeople()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
var vaults = shell.Vaults;
|
||||
server.AddAccount("bob@example.com", "Bob Example");
|
||||
|
||||
await CreateVaultAsync(vaults, "Alone");
|
||||
var alone = vaults.SelectedVault!.VaultId;
|
||||
|
||||
await CreateVaultAsync(vaults, "Shared with Bob");
|
||||
var shared = vaults.SelectedVault!.VaultId;
|
||||
|
||||
vaults.NewMemberEmail = "bob@example.com";
|
||||
await vaults.AddMemberCommand.ExecuteAsync(null);
|
||||
vaults.Members.Count.ShouldBe(2, vaults.Status);
|
||||
|
||||
vaults.SelectedVault = vaults.Vaults.Single(row => row.VaultId == alone);
|
||||
await EventuallyAsync(() => vaults.Members.Count == 1, "the vault on its own has only its owner");
|
||||
|
||||
var gate = new TaskCompletionSource();
|
||||
server.MemberReadGate = gate;
|
||||
|
||||
var reload = vaults.LoadAsync(Token);
|
||||
|
||||
vaults.IsBusy.ShouldBeTrue("the reload is what holds the screen busy while its read is held");
|
||||
|
||||
vaults.SelectedVault = vaults.Vaults.Single(row => row.VaultId == shared);
|
||||
|
||||
gate.SetResult();
|
||||
server.MemberReadGate = null;
|
||||
await reload;
|
||||
|
||||
await EventuallyAsync(() => vaults.Members.Count == 2, "Bob is in the vault now showing");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Through the form rather than straight at the command, because the name is what the form is for —
|
||||
/// and because the form is now the only way in: there is no separate "make a team" step behind it.
|
||||
/// </remarks>
|
||||
/// <summary>Waits for something a fire-and-forget read will make true, within a bound.</summary>
|
||||
private static async Task EventuallyAsync(Func<bool> condition, string because)
|
||||
{
|
||||
var deadline = TimeProvider.System.GetUtcNow().AddSeconds(10);
|
||||
|
||||
while (TimeProvider.System.GetUtcNow() < deadline)
|
||||
{
|
||||
if (condition())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(20), Token);
|
||||
}
|
||||
|
||||
throw new ShouldAssertException(because);
|
||||
}
|
||||
|
||||
/// <summary>The armour a key is stored in, which this suite never parses and only round-trips.</summary>
|
||||
private static string PrivateKey(string body) =>
|
||||
$"-----BEGIN OPENSSH PRIVATE KEY-----\n{body}\n-----END OPENSSH PRIVATE KEY-----\n";
|
||||
|
||||
Reference in new issue
Block a user