diff --git a/docs/platform-flags.md b/docs/platform-flags.md
index 7dc117b..3e91090 100644
--- a/docs/platform-flags.md
+++ b/docs/platform-flags.md
@@ -424,13 +424,38 @@ be honest about: the cache offers no protection against another process running
## Build and CI
-**The layout suite needs `libfontconfig` installed, and nothing else.** Avalonia's headless renderer is
-still Skia, and `libSkiaSharp.so` — which the test project copies into its own output — links against
-it. On an image without it, every one of the 69 tests fails inside `HeadlessUnitTestSession` before
-measuring anything, reported as a `TypeInitializationException` on `SkiaSharp.SKImageInfo` that names
-none of the tests' actual subjects. The CI job installs it. Worth being precise that this is the
-library and not fonts: verified in a container where `fc-list` returns zero and the suite passes anyway,
-because the application carries Inter itself.
+**The layout suite needs two unrelated things on a bare image, and each hides the other.** Both were
+found the slow way, one per CI run, because the first masks the second entirely.
+
+The first is `libfontconfig`. Avalonia's headless renderer is Skia, and the `libSkiaSharp.so` the test
+project copies into its own output links against it; without it the native library never loads and all
+69 tests fail inside `HeadlessUnitTestSession` with a `TypeInitializationException` on
+`SkiaSharp.SKImageInfo` naming none of their actual subjects. The CI job installs the package.
+
+The second only becomes visible once the first is fixed, and is not about a package at all. Avalonia
+takes its default font family from the platform, and on an image with no fonts installed there is no
+answer — `FontManager` throws "Default font family name can't be null or empty" during
+`AppBuilder.SetupUnsafe`, again before any test body runs and again for all 69. `WithInterFont` does not
+help by itself: it registers a collection without naming a default. Both `Program.BuildAvaloniaApp` and
+the layout suite's `HeadlessApp` now set `FontManagerOptions.DefaultFamilyName` to
+`avares://Avalonia.Fonts.Inter/Assets#Inter` explicitly, which owes the host nothing because the font
+travels in the package.
+
+Pinning it is worth more than the CI fix. A suite that measures text was taking its metrics from
+whatever the machine happened to have — Segoe UI on Windows, DejaVu on Linux — and reporting both as
+one number. *Verified* on Alpine musl with `fc-list` returning zero and on a Fedora desktop with 595
+fonts, which now agree. **Unverified on Windows:** the pinning changed the metrics there too, so a
+tight assertion could conceivably have moved.
+
+**The end-to-end suite must state its plaintext exemption rather than inherit it.** `ServerConnection`
+allows an `http` OIDC authority only when it is loopback, which is a sound rule the suite cannot lean
+on: Testcontainers reports the host the container is actually reachable at, so running the tests
+directly gives `localhost` and passes, while running them *inside* a container — which is what a
+containerised CI runner does — gives the bridge gateway `172.17.0.1` and is refused. That refusal is
+the product being correct; a client that quietly accepted plaintext metadata from a routable address
+would be a real weakness. `M1VerticalSliceTests` therefore passes `configureOidc` to set
+`RequireHttpsMetadata = false` for the throwaway Keycloak it starts itself, and the rule stays as strict
+as it was for everyone else.
**Integration tests need a Docker daemon** (Testcontainers). They run on `ubuntu-latest` in CI.
macOS runners have no Docker daemon, and the Windows CI job is deliberately build-only. So
diff --git a/src/DodoSSH.Client.App/Program.cs b/src/DodoSSH.Client.App/Program.cs
index bfad2f0..a7c8863 100644
--- a/src/DodoSSH.Client.App/Program.cs
+++ b/src/DodoSSH.Client.App/Program.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Media;
namespace DodoSSH.Client.App;
@@ -16,9 +17,18 @@ internal static class Program
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
/// Used by the designer as well as by .
+ ///
+ /// WithInterFont registers Inter; it does not make it the default, and until this line the
+ /// application shipped a font it then declined to use — falling back to Segoe UI on Windows and to
+ /// whatever fontconfig offered on Linux. Almost nothing visible moves, because App.axaml sets
+ /// MonoFont on essentially every control that draws text, but the fallback behind those is now
+ /// a font that travels with the build rather than one the machine is assumed to have. The layout
+ /// suite pins the same family, and has to: see HeadlessApp.
+ ///
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure()
.UsePlatformDetect()
.WithInterFont()
+ .With(new FontManagerOptions { DefaultFamilyName = "avares://Avalonia.Fonts.Inter/Assets#Inter" })
.LogToTrace();
}
diff --git a/tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs b/tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs
index 70db435..552185f 100644
--- a/tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs
+++ b/tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs
@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Headless;
+using Avalonia.Media;
using DodoSSH.Client.App.Layout.Tests;
[assembly: AvaloniaTestApplication(typeof(HeadlessApp))]
@@ -28,9 +29,34 @@ namespace DodoSSH.Client.App.Layout.Tests;
internal static class HeadlessApp
{
/// Found by name; is the contract.
+ ///
+ ///
+ /// The default family is named explicitly, because the paragraph above was describing an intention
+ /// rather than what happened. WithInterFont registers a font collection; it does not make it
+ /// the default. Avalonia resolves that from the platform — which is to say from whatever the machine
+ /// has installed — so a suite whose whole job is measuring text was measuring Segoe UI on a
+ /// developer's Windows and DejaVu on their Linux, and treating the two numbers as one.
+ ///
+ ///
+ /// On a machine with no fonts at all it does not merely differ, it stops: FontManager throws
+ /// "Default font family name can't be null or empty" during AppBuilder.SetupUnsafe, before any
+ /// test body runs. That is how a container CI runner reports all sixty-eight of these as failures with
+ /// not one assertion among them. Naming Inter costs the runner nothing, because the font travels
+ /// inside the package rather than on the host.
+ ///
+ ///
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure()
.UseSkia()
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false })
- .WithInterFont();
+ .WithInterFont()
+ .With(new FontManagerOptions { DefaultFamilyName = InterFamily });
+
+ /// The family WithInterFont registers.
+ ///
+ /// Kept in step with Program.BuildAvaloniaApp by hand, and it matters that they agree: a
+ /// harness pinning a different default from the one the application runs with would certify heights
+ /// for a window nobody sees, which is the failure the opening paragraph of this file is about.
+ ///
+ internal const string InterFamily = "avares://Avalonia.Fonts.Inter/Assets#Inter";
}
diff --git a/tests/DodoSSH.SystemTests/M1VerticalSliceTests.cs b/tests/DodoSSH.SystemTests/M1VerticalSliceTests.cs
index d2231c4..b71bbdc 100644
--- a/tests/DodoSSH.SystemTests/M1VerticalSliceTests.cs
+++ b/tests/DodoSSH.SystemTests/M1VerticalSliceTests.cs
@@ -61,8 +61,21 @@ public sealed class M1VerticalSliceTests(DevStack stack) : IClassFixture options with { RequireHttpsMetadata = false });
AssertDiscoveredFromTheServer(connection);