diff --git a/app/Build/Commands/VerifyCommand.cs b/app/Build/Commands/VerifyCommand.cs
index a8b37992a..05007f172 100644
--- a/app/Build/Commands/VerifyCommand.cs
+++ b/app/Build/Commands/VerifyCommand.cs
@@ -94,6 +94,15 @@ public sealed class VerifyCommand
var directory = Environment.GetTrimSmokeTestDirectory();
var publishDirectory = Path.Combine(directory, "bin", "verify");
+
+ //
+ // Publishing leaves the files of an earlier run in place, among them copies of assemblies which the trimmer
+ // now removes completely. The dependency manifest still lists such an assembly, so a leftover copy can be
+ // loaded in its place and hide exactly what the smoke test is there to find:
+ //
+ if (Directory.Exists(publishDirectory))
+ Directory.Delete(publishDirectory, recursive: true);
+
var publishExitCode = await CommandRunner.RunAsync(directory, "dotnet", $"publish --configuration release --runtime {rid.AsMicrosoftRid()} --output \"{publishDirectory}\" --nologo");
if (publishExitCode is not 0)
return publishExitCode;
diff --git a/app/TrimSmokeTest/AppJsonOptions.cs b/app/TrimSmokeTest/AppJsonOptions.cs
new file mode 100644
index 000000000..b90ab0689
--- /dev/null
+++ b/app/TrimSmokeTest/AppJsonOptions.cs
@@ -0,0 +1,54 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace AIStudio.TrimSmokeTest;
+
+///
+/// Finds the options with which AI Studio reads and writes JSON.
+///
+///
+/// AI Studio keeps each kind of options in a static field or property, next to the code which uses them, so
+/// these are the options which reach System.Text.Json. A list written by hand would miss the next new one.
+/// The tests of the app compile this file, too, see Tests.csproj, so that both check the same options.
+///
+public static class AppJsonOptions
+{
+ private const BindingFlags STATIC_MEMBERS = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
+
+ ///
+ /// Returns the options of every static field and property of AI Studio which holds some.
+ ///
+ ///
+ /// Reading a member runs the type initializer of its type. Hence, the static state which the start of
+ /// the app fills has to be there before, Program.LOGGER_FACTORY above all.
+ ///
+ /// The options by the name of their type and member, e.g., SettingsManager.JSON_OPTIONS.
+ public static IReadOnlyDictionary FindAll()
+ {
+ var found = new SortedDictionary(StringComparer.Ordinal);
+ foreach (var type in typeof(AIStudio.Program).Assembly.GetTypes())
+ {
+ //
+ // A context keeps options of its own for its Default instance, which never reach System.Text.Json
+ // directly. The static members of an open generic type hold no value at all:
+ //
+ if (type.ContainsGenericParameters || type.IsSubclassOf(typeof(JsonSerializerContext)))
+ continue;
+
+ //
+ // The field behind a static auto-property is found as the property already:
+ //
+ foreach (var field in type.GetFields(STATIC_MEMBERS))
+ if (field.FieldType == typeof(JsonSerializerOptions) && !field.IsDefined(typeof(CompilerGeneratedAttribute)) && field.GetValue(null) is JsonSerializerOptions options)
+ found.Add($"{type.Name}.{field.Name}", options);
+
+ foreach (var property in type.GetProperties(STATIC_MEMBERS))
+ if (property.PropertyType == typeof(JsonSerializerOptions) && property.GetMethod is not null && property.GetValue(null) is JsonSerializerOptions options)
+ found.Add($"{type.Name}.{property.Name}", options);
+ }
+
+ return found;
+ }
+}
\ No newline at end of file