Merge main into the teams branch

Two conflicts, and both were two people counting the same things differently
rather than disagreeing about what the code should do.

PhoneShell's header comment. The branch made "the five hub screens"
numberless, because TEAMS made it six and a number in that sentence had
already gone stale once. Main corrected "three destinations" to "two" in the
same sentence, because giving a shell the whole phone took the terminal out of
the set the header is drawn on. Both are right and neither noticed the other:
the header now stays on the hub's screens and on the two top-level
destinations, which is Hosts and Keychain.

The manual checks. Both sides appended a Phase 10 — main added the software
keyboard and the phone's terminal surface as 10 and 11, the branch added
Teams. Nothing about them overlaps, so the resolution is to keep all three in
the order they were written and renumber Teams to Phase 12, its subsections
and the one cross-reference inside 12.1 with it. Main's two phases keep the
numbers they already carry in its history, since renumbering those would move
headings somebody may already have linked to.

Everything else merged without a conflict, and the two places worth checking
afterwards both held: IsMoreSurface and the first case of
PhoneShell.OnBackRequested each kept ShellScreen.Team alongside main's edits.
Those two are one fact in two places, so a merge that dropped Team from either
would have trapped the user on the teams screen with the MORE tab dark.

Verified after resolving: solution builds with no errors and no new warnings,
the Android head builds, and every suite passes — App 214, Layout 73, Api 162,
Infrastructure 34, Contracts 25, Session 54. App gained the three shell-flow
tests main brought with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 14:40:27 +02:00
co-authored by Claude Opus 5
15 changed files with 882 additions and 236 deletions
@@ -303,20 +303,15 @@ public sealed class ShellFlowTests : IAsyncLifetime
/// application is not running.
/// </para>
/// <para>
/// Both branches are written out rather than compared against the constant itself. Asserting a
/// constant against itself would pass however it were edited, and the whole point of this test is
/// that a release build must not ship a developer's loopback address — or, since the split, that a
/// debug build must not point a clone at production.
/// The address is written out rather than compared against the constant itself. Asserting a constant
/// against itself would pass however it were edited, and the whole point of this test is that no
/// build ships a developer's loopback address.
/// </para>
/// </remarks>
[Fact]
public void TheDefaultServerUrl_IsTheHostedDeployment_ExceptInADebugBuild()
public void TheDefaultServerUrl_IsTheHostedDeployment_InEveryBuild()
{
#if DEBUG
shell.ServerUrl.ShouldBe("http://localhost:5233");
#else
shell.ServerUrl.ShouldBe("https://ssh.dodotech.cloud");
#endif
}
[Theory]
@@ -687,6 +682,82 @@ public sealed class ShellFlowTests : IAsyncLifetime
shell.Screen.ShouldBe(ShellScreen.Vault);
}
/// <remarks>
/// The phone's connect menu is drawn over the terminal's own rectangle, so it obeys the rule the palette
/// does: whatever covers the renderer collapses it instead. The surface stays, because the bar the menu
/// was raised from is part of it — see <c>MainWindowViewModel.IsTerminalShowing</c>.
/// </remarks>
[Fact]
public async Task TheConnectSheet_HidesTheRendererAndLeavesTheSurfaceUnderIt()
{
var vault = await ReadyToConnectAsync();
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
await vault.ConnectCommand.ExecuteAsync(null);
shell.OpenConnectSheetCommand.Execute(null);
shell.IsConnectSheetOpen.ShouldBeTrue();
shell.IsTerminalShowing.ShouldBeFalse("the sheet draws over the renderer's rectangle");
shell.IsTerminalSurface.ShouldBeTrue("the bar the sheet was raised from is on that surface");
shell.CloseConnectSheetCommand.Execute(null);
shell.IsTerminalShowing.ShouldBeTrue();
}
/// <remarks>
/// The flag holds the renderer blank, so one set while a page was showing would be a sheet nobody can
/// see keeping a terminal hidden that nothing would put back.
/// </remarks>
[Fact]
public async Task TheConnectSheet_RefusesToOpenOverAPage()
{
var vault = await ReadyToConnectAsync();
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
await vault.ConnectCommand.ExecuteAsync(null);
shell.ShowScreenCommand.Execute(ShellScreen.Vault);
shell.OpenConnectSheetCommand.Execute(null);
shell.IsConnectSheetOpen.ShouldBeFalse();
}
/// <remarks>
/// Every entry on the menu navigates, and none of them closes the sheet itself: leaving the terminal
/// surface is what lowers it. That is the guarantee worth a test — it is what makes routes nobody wrote
/// the sheet for, like closing the last tab or locking, safe.
/// </remarks>
[Fact]
public async Task LeavingTheTerminal_LowersTheConnectSheetHoweverItIsLeft()
{
var vault = await ReadyToConnectAsync();
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
await vault.ConnectCommand.ExecuteAsync(null);
// The menu's own second entry: one screen, over one view model, with the kind of remote chosen by
// the thing that navigates.
shell.OpenConnectSheetCommand.Execute(null);
shell.ShowFilesCommand.Execute(RemoteKind.Bucket);
shell.IsConnectSheetOpen.ShouldBeFalse();
shell.IsBucketsShowing.ShouldBeTrue();
// And a route the sheet was never wired to: back to the terminal, open it, then end the only shell
// there is.
shell.ShowTerminalCommand.Execute(null);
shell.OpenConnectSheetCommand.Execute(null);
shell.IsConnectSheetOpen.ShouldBeTrue();
await shell.CloseTabCommand.ExecuteAsync(shell.Tabs[0]);
shell.IsConnectSheetOpen.ShouldBeFalse("closing the last tab returns the surface to a page");
shell.IsShowingPages.ShouldBeTrue();
}
/// <remarks>
/// A visible WebView with no pane in it reads as the application having broken, so this is the one
/// transition that moves the surface back on its own.