Sweep out the group-card navigation nothing reaches any more

This commit is contained in:
2026-08-07 18:14:54 +02:00
parent dca2e888d6
commit 281e828e25
5 changed files with 115 additions and 596 deletions
@@ -33,8 +33,8 @@ namespace DodoSSH.Client.App.Layout.Tests;
/// editor or the chosen-hosts menu's "Change group…" rather than a drag. The card-navigation surface this
/// was written against — <c>OpenGroupCommand</c>, <c>VisibleGroups</c>, <c>GroupTrail</c>,
/// <c>SelectedGroup</c>, <c>GroupCrumbViewModel</c>, and the drag command <c>MoveHostToGroupCommand</c> dragged
/// onto — is no longer reached from either head's markup; <c>GroupFilter</c> stays, since group-heading
/// commands still fall back to it. Section-level
/// onto — is no longer on <c>VaultViewModel</c> at all, confirmed unreachable from both heads' markup before
/// removal; <c>GroupFilter</c> stays, since group-heading commands still fall back to it. Section-level
/// coverage — flattening order, the headerless invariant, per-section collapse, filter composition, monogram
/// stability — is in <c>HostSectionsTests</c> instead, which is where a heading now belongs.
/// </para>
@@ -1342,8 +1342,7 @@ public sealed class VaultSharingTests : IAsyncLifetime
group.VaultId.ShouldBe(sharedVaultId, vault.Status);
group.VaultBadge.ShouldBe("PLATFORM SECRETS", "a card in a session holding two vaults says which");
vault.SelectedGroup = group;
vault.EditGroupCommand.Execute(null);
vault.EditGroupCommand.Execute(group);
vault.ShowsGroupEditorVaultChoice.ShouldBeFalse("an item cannot be moved between vaults");
@@ -1484,10 +1483,9 @@ public sealed class VaultSharingTests : IAsyncLifetime
production.VaultId.ShouldNotBe(sharedVaultId, "this test is meaningless with both in one vault");
production.Group.ParentId.ShouldNotBeNull("it was nested, which is what has to stay behind");
// As the card's menu does before it runs the command; see HostsScreen.OnGroupContextRequested.
vault.SelectedGroup = production;
vault.MoveGroupCommand.Execute(null);
// Passed directly, the way MoveGroupFromHeading resolves and hands in a row now that there are no
// group cards to select one from.
vault.MoveGroupCommand.Execute(production);
vault.IsMovingGroup.ShouldBeTrue(vault.Status);
vault.MoveGroupVaultChoices.ShouldNotContain(choice => choice.VaultId == production.VaultId);
@@ -1520,7 +1518,6 @@ public sealed class VaultSharingTests : IAsyncLifetime
// The group it was nested under is the one thing that stayed, and it stayed where it was.
Named(vault, "estate").VaultId.ShouldBe(production.VaultId);
vault.SelectedGroup?.EntityId.ShouldBe(moved.EntityId, "the buttons follow the group they moved");
vault.Status.ShouldContain("Platform secrets");
vault.Status.ShouldContain("top level");
}
@@ -1544,9 +1541,7 @@ public sealed class VaultSharingTests : IAsyncLifetime
vault.GroupEditorLabel = "production";
await vault.SaveGroupCommand.ExecuteAsync(null);
vault.SelectedGroup = vault.Groups.ShouldHaveSingleItem();
vault.MoveGroupCommand.Execute(null);
vault.MoveGroupCommand.Execute(vault.Groups.ShouldHaveSingleItem());
vault.IsMovingGroup.ShouldBeFalse();
vault.MoveGroupVaultChoices.ShouldBeEmpty();
@@ -1655,66 +1650,13 @@ public sealed class VaultSharingTests : IAsyncLifetime
/// <remarks>
/// <para>
/// Dragging a host card onto a group card is the one gesture that files a host without opening its
/// editor, and it can now be aimed across a vault boundary, because both grids draw every readable
/// vault. The write it would make is the exact thing the host editor's group picker was fixed to
/// prevent: an id only the other vault's holders can resolve.
/// </para>
/// <para>
/// Refused and said so, rather than quietly treated as "no group" — the user is plainly filing
/// something, and unfiling it instead would be the wrong answer delivered silently.
/// </para>
/// </remarks>
[Fact]
public async Task AHostDraggedOntoAnotherVaultsGroup_IsRefusedRatherThanFiledUnderIt()
{
await UnlockedAsync();
var vaults = shell.Vaults;
await CreateVaultAsync(vaults, "Platform secrets");
var vault = shell.Vault!;
var sharedVaultId = vaults.SelectedVault!.VaultId;
await vault.LoadAsync(Token);
vault.NewGroupCommand.Execute(null);
vault.GroupEditorSelectedVault =
vault.GroupEditorVaultChoices.Single(choice => choice.VaultId == sharedVaultId);
vault.GroupEditorLabel = "production";
await vault.SaveGroupCommand.ExecuteAsync(null);
// The host stays in the personal vault, which is where a new one goes without being told otherwise.
vault.NewHostCommand.Execute(null);
vault.EditorLabel = "prod-db";
vault.EditorHostname = "db.internal";
await vault.SaveHostCommand.ExecuteAsync(null);
var host = vault.Hosts.Single(row => string.Equals(row.Label, "prod-db", StringComparison.Ordinal));
var group = vault.Groups.Single(row => row.VaultId == sharedVaultId);
host.VaultId.ShouldNotBe(sharedVaultId, "this test is meaningless with both in one vault");
await vault.MoveHostToGroupCommand.ExecuteAsync(new HostGroupMove(host, group.EntityId));
vault.Status.ShouldContain("its own vault");
vault.Hosts
.Single(row => string.Equals(row.Label, "prod-db", StringComparison.Ordinal))
.Host.GroupId
.ShouldBeNull("the host is left where it was rather than filed under an unresolvable group");
}
/// <remarks>
/// <para>
/// The same rule as the drag above, arrived at from the other end: a group lives in exactly one vault, so
/// + NEW HOST inside one has to open on that vault as well as on that group. The two defaults were
/// decided separately — the group came from the screen, the vault from the standing "new items go to"
/// preference — so pressing the button inside a shared vault's group opened a form bound for the personal
/// vault, with the group silently dropped by the picker that keeps the two in step.
/// A group lives in exactly one vault, so + NEW HOST inside one has to open on that vault as well as on
/// that group. The two defaults were decided separately — the group from <see cref="VaultViewModel.GroupTarget"/>,
/// the vault from the standing "new items go to" preference — so pressing the button inside a shared
/// vault's group opened a form bound for the personal vault, with the group silently dropped by the
/// picker that keeps the two in step. <c>GroupFilter</c> is written directly here rather than through
/// <c>OpenGroupCommand</c>, which left with the group cards it used to open — see that property's own
/// remarks.
/// </para>
/// <para>
/// The second half is the same test read backwards, and it is what says which of the two wins. The
@@ -1752,7 +1694,7 @@ public sealed class VaultSharingTests : IAsyncLifetime
vault.GroupEditorLabel = "production";
await vault.SaveGroupCommand.ExecuteAsync(null);
vault.OpenGroupCommand.Execute(Named(vault, "production"));
vault.GroupFilter = Named(vault, "production");
vault.NewHostCommand.Execute(null);
@@ -1773,7 +1715,7 @@ public sealed class VaultSharingTests : IAsyncLifetime
// And backwards: the preference names the shared vault and the open group is in the personal one.
vault.SelectedTargetVault = vault.TargetVaults.Single(choice => choice.VaultId == sharedVaultId);
vault.OpenGroupCommand.Execute(Named(vault, "staging"));
vault.GroupFilter = Named(vault, "staging");
vault.NewHostCommand.Execute(null);