mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-03 19:07:21 +00:00
Fixed chats not loading in the v26.10.1 pre-release and added a trim smoke test (#1128)
This commit is contained in:
1 parent
7b9e96d6a1
commit
fd0a42fa8a
12 files changed
+346
-14
No files matched your search
@@ -0,0 +1,61 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace AIStudio.TrimSmokeTest;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the contract System.Text.Json uses for a root and for everything it holds.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A type whose contract needs code the trimmer removed fails right here, before any value is read or
|
||||
/// written, just like it fails in AI Studio. A chat which lacks the property in question fails all the same.
|
||||
/// </remarks>
|
||||
public static class JsonContractWalker
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a description of every contract which cannot be built, starting at the given root.
|
||||
/// </summary>
|
||||
/// <param name="root">The type to start with, and the options AI Studio uses for it.</param>
|
||||
/// <returns>One line per failure, naming the path from the root to the type which failed.</returns>
|
||||
public static IReadOnlyList<string> FindBrokenContracts(JsonRoot root)
|
||||
{
|
||||
//
|
||||
// The same System.Text.Json does with the first call of Serialize or Deserialize:
|
||||
//
|
||||
root.Options.MakeReadOnly(populateMissingResolver: true);
|
||||
|
||||
var failures = new List<string>();
|
||||
Visit(root.Type, root.Type.Name, root.Options, [], failures);
|
||||
return failures;
|
||||
}
|
||||
|
||||
private static void Visit(Type type, string path, JsonSerializerOptions options, HashSet<Type> visited, List<string> failures)
|
||||
{
|
||||
if (!visited.Add(type))
|
||||
return;
|
||||
|
||||
JsonTypeInfo typeInfo;
|
||||
try
|
||||
{
|
||||
typeInfo = options.GetTypeInfo(type);
|
||||
}
|
||||
catch (Exception exception) when (exception is NotSupportedException or InvalidOperationException)
|
||||
{
|
||||
failures.Add($"{path}: {exception.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var property in typeInfo.Properties)
|
||||
Visit(property.PropertyType, $"{path} → {property.Name}", options, visited, failures);
|
||||
|
||||
if (typeInfo.KeyType is { } keyType)
|
||||
Visit(keyType, $"{path} → key", options, visited, failures);
|
||||
|
||||
if (typeInfo.ElementType is { } elementType)
|
||||
Visit(elementType, $"{path} → element", options, visited, failures);
|
||||
|
||||
if (typeInfo.PolymorphismOptions is { } polymorphism)
|
||||
foreach (var derivedType in polymorphism.DerivedTypes)
|
||||
Visit(derivedType.DerivedType, $"{path} → {derivedType.DerivedType.Name}", options, visited, failures);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AIStudio.TrimSmokeTest;
|
||||
|
||||
/// <summary>
|
||||
/// A type which AI Studio reads or writes as JSON on its own, together with the options it uses for that.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type which gets serialized. Whatever it holds gets checked along with it.</param>
|
||||
/// <param name="Options">The options AI Studio passes to System.Text.Json for this type.</param>
|
||||
public sealed record JsonRoot(Type Type, JsonSerializerOptions Options);
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Agents;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Assistants.Builder;
|
||||
using AIStudio.Assistants.PromptOptimizer;
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
using AIStudio.Assistants.VisualBriefing;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Settings.DataModel.PreviousModels;
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.DataBackup;
|
||||
|
||||
namespace AIStudio.TrimSmokeTest;
|
||||
|
||||
/// <summary>
|
||||
/// The types which AI Studio reads or writes as JSON on their own, not only as part of another type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whatever these types hold, i.e., the types of their properties, the elements of their collections, and
|
||||
/// their derived types, gets checked along with them, so only the outermost types belong here. Add a type
|
||||
/// whenever AI Studio starts to store a new kind of file or to read a new kind of model answer.<br/><br/>
|
||||
///
|
||||
/// The answers of models are read with options of their own, which are private and only change names and
|
||||
/// leniency. The default options build the same contracts for the types of .NET they hold. A type which is
|
||||
/// private to its parser, such as the DTO of a slide deck, is out of reach here.
|
||||
/// </remarks>
|
||||
public static class JsonRoots
|
||||
{
|
||||
public static readonly IReadOnlyList<JsonRoot> ALL =
|
||||
[
|
||||
// Chats, in the workspaces, among the temporary chats, and in chat archives:
|
||||
new(typeof(ChatThread), WorkspaceBehaviour.JSON_OPTIONS),
|
||||
|
||||
// The settings, and their older versions, which the migrations still read:
|
||||
new(typeof(Data), SettingsManager.JSON_OPTIONS),
|
||||
new(typeof(DataV1V3), SettingsManager.JSON_OPTIONS),
|
||||
new(typeof(DataV4), SettingsManager.JSON_OPTIONS),
|
||||
new(typeof(DataV5), SettingsManager.JSON_OPTIONS),
|
||||
|
||||
// The projects of the Visual Briefing assistant:
|
||||
new(typeof(VisualBriefingManifest), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingBuildRecord), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingEvidenceArtifact), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingPlanArtifact), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingContentArtifact), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingPresentationArtifact), VisualBriefingJson.Persistence),
|
||||
|
||||
// Backups and their restore:
|
||||
new(typeof(DataBackupManifest), DataBackupFormat.JSON_OPTIONS),
|
||||
new(typeof(DataRestorePlan), DataBackupFormat.JSON_OPTIONS),
|
||||
new(typeof(DataRestoreReport), DataBackupFormat.JSON_OPTIONS),
|
||||
|
||||
// Answers of models:
|
||||
new(typeof(LuaResponse), JsonSerializerOptions.Default),
|
||||
new(typeof(LauncherTextsResponse), JsonSerializerOptions.Default),
|
||||
new(typeof(PromptOptimizationResult), JsonSerializerOptions.Default),
|
||||
new(typeof(RetrievalContextValidationResult), JsonSerializerOptions.Default),
|
||||
new(typeof(AssistantAuditResult), JsonSerializerOptions.Default),
|
||||
new(typeof(SwotCategoryAnalysisResult), JsonSerializerOptions.Default),
|
||||
new(typeof(SwotModerationResult), JsonSerializerOptions.Default),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Assistants.VisualBriefing;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.TrimSmokeTest;
|
||||
|
||||
/// <summary>
|
||||
/// Writes and reads filled values of the roots which matter most, since some parts of a contract only come
|
||||
/// into play once there is a value, e.g., the factory which builds an immutable collection.
|
||||
/// </summary>
|
||||
public static class JsonRoundTrips
|
||||
{
|
||||
private const string TEXT_WITH_WEB_ADDRESS = "The details are on https://example.org/chosen.";
|
||||
|
||||
/// <summary>
|
||||
/// Returns a description of every value which does not survive being written and read again.
|
||||
/// </summary>
|
||||
/// <returns>One line per failure, naming the type which failed.</returns>
|
||||
public static IReadOnlyList<string> FindLosses()
|
||||
{
|
||||
var restrictions = ChatRestrictions.NONE.WithWebAddressesWrittenByModelIn(TEXT_WITH_WEB_ADDRESS);
|
||||
var chat = new ChatThread
|
||||
{
|
||||
Name = "Trim smoke test",
|
||||
Blocks =
|
||||
[
|
||||
new ContentBlock
|
||||
{
|
||||
Time = DateTimeOffset.UnixEpoch,
|
||||
ContentType = ContentType.TEXT,
|
||||
Content = new ContentText { Text = TEXT_WITH_WEB_ADDRESS },
|
||||
Role = ChatRole.USER,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chat.RequireRestrictions(restrictions);
|
||||
|
||||
var failures = new List<string>();
|
||||
AddLoss(chat, WorkspaceBehaviour.JSON_OPTIONS, failures);
|
||||
AddLoss(new Data(), SettingsManager.JSON_OPTIONS, failures);
|
||||
AddLoss(new VisualBriefingManifest { Name = "Trim smoke test", Restrictions = restrictions }, VisualBriefingJson.Persistence, failures);
|
||||
return failures;
|
||||
}
|
||||
|
||||
private static void AddLoss<T>(T value, JsonSerializerOptions options, List<string> failures)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value, options);
|
||||
var jsonAgain = JsonSerializer.Serialize(JsonSerializer.Deserialize<T>(json, options), options);
|
||||
if (jsonAgain != json)
|
||||
failures.Add($"{typeof(T).Name}: written again after reading, it gives other JSON than before.");
|
||||
}
|
||||
catch (Exception exception) when (exception is NotSupportedException or InvalidOperationException or JsonException)
|
||||
{
|
||||
failures.Add($"{typeof(T).Name}: {exception.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.TrimSmokeTest;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a trimmed build can still read and write the JSON of AI Studio, see TrimSmokeTest.csproj.
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
public static int Main()
|
||||
{
|
||||
//
|
||||
// Several types of the app initialize a static logger from this factory, which only the start of
|
||||
// the app assigns. Without it, they die in their type initializer, see app/Tests/TestHost.cs:
|
||||
//
|
||||
AIStudio.Program.LOGGER_FACTORY = NullLoggerFactory.Instance;
|
||||
|
||||
var failures = new List<string>();
|
||||
foreach (var root in JsonRoots.ALL)
|
||||
failures.AddRange(JsonContractWalker.FindBrokenContracts(root));
|
||||
|
||||
failures.AddRange(JsonRoundTrips.FindLosses());
|
||||
if (failures.Count is 0)
|
||||
{
|
||||
Console.WriteLine($"- All {JsonRoots.ALL.Count} JSON roots can be read and written.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var failure in failures)
|
||||
Console.WriteLine($"- Error: {failure}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
Checks what only a published build of AI Studio lacks. The trimmer removes code of .NET which System.Text.Json
|
||||
reaches by reflection only, while debug builds and the tests are not trimmed. Hence, this program has to run
|
||||
published, trimmed by the same Trimming.props as the app; `dotnet run verify` does both.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<RootNamespace>AIStudio.TrimSmokeTest</RootNamespace>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<SelfContained>true</SelfContained> <!-- Trimming works for self-contained builds only -->
|
||||
|
||||
<!-- IL2026: Usage of methods marked as RequiresUnreferencedCode. Finding out what this means for a trimmed build is the whole point of this program. -->
|
||||
<NoWarn>IL2026</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\Trimming.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MindWork AI Studio\MindWork AI Studio.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in new issue
Block a user