Upgraded to .NET 10 (#1147)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-01 20:32:16 +01:00
1 parent faae2951f7
commit fc7399e81b
327 files changed
+3059 -3049

No files matched your search

+2 -2
View File
@@ -339,7 +339,7 @@ jobs:
-y \
flathub \
"org.freedesktop.Sdk//${FLATPAK_FREEDESKTOP_VERSION}" \
"org.freedesktop.Sdk.Extension.dotnet9//${FLATPAK_FREEDESKTOP_VERSION}"
"org.freedesktop.Sdk.Extension.dotnet10//${FLATPAK_FREEDESKTOP_VERSION}"
tools_dir="$RUNNER_TEMP/flatpak-sync-tools"
mkdir -p "$tools_dir"
@@ -793,7 +793,7 @@ jobs:
- name: Read the toolchain versions from the metadata
run: |
# The .NET SDK version. The format is '9.0.205 (commit 3e1383b780)',
# The .NET SDK version. The format is '10.0.114 (commit 0427b9abbe)',
# so we extract the version number alone:
dotnet_sdk_version=$(sed -n '4p' metadata.txt | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')
+4 -4
View File
@@ -81,7 +81,7 @@ MindWork AI Studio is a cross-platform desktop application for interacting with
**Key Architecture Points:**
- **Runtime:** Rust-based Tauri v2 application providing the native window, system integration, and IPC layer
- **App:** .NET 9 Blazor Server application providing the UI and core functionality
- **App:** .NET 10 Blazor Server application providing the UI and core functionality
- **Communication:** The Rust runtime and .NET app communicate via HTTPS with TLS certificates generated at startup
- **Providers:** Multi-provider architecture supporting OpenAI, Anthropic, Google, Mistral, Perplexity, self-hosted models, and others
- **Plugin System:** Lua-based plugin system for language packs, configuration, assistants, and model knowledge
@@ -89,7 +89,7 @@ MindWork AI Studio is a cross-platform desktop application for interacting with
## Building
### Prerequisites
- .NET 9 SDK
- .NET 10 SDK
- Rust toolchain (stable)
- Tauri v2 CLI
- Tauri prerequisites (platform-specific dependencies)
@@ -114,7 +114,7 @@ failures (for reference: https://github.com/openai/codex/issues/4915). This appl
Instead, use the JetBrains IDE MCP servers. They execute in the IDE process, which runs outside the
agent sandbox:
- `rider` for the .NET solution at `app/MindWork AI Studio.sln`
- `rider` for the .NET solution at `app/MindWork AI Studio.slnx`
- `rustrover` for the Rust runtime at `runtime/`
Pass the `rootFolder` parameter on every call, e.g. the absolute path of the `app` directory for Rider
@@ -169,7 +169,7 @@ again yourself when `metadata.txt` changes during the session, e.g., after a mer
**Building and testing** happen in the shell:
- The quality gate, the same as in the pipeline: `cd app/Build && dotnet run verify`. It compiles both
the .NET app and the Rust runtime on its way. Narrower checks: `dotnet build "app/MindWork AI Studio.sln"`,
the .NET app and the Rust runtime on its way. Narrower checks: `dotnet build "app/MindWork AI Studio.slnx"`,
`dotnet test` in `app/Tests`, and `cargo test` or `cargo clippy --all-targets -- -D warnings` in `runtime/`.
- A cold build of the Rust runtime takes several minutes: run the gate and cargo in the background, or
give them the longest timeout available.
+2 -2
View File
@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="[2] Start .NET Server" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/MindWork AI Studio/bin/Debug/net9.0/osx-arm64/mindworkAIStudio" />
<option name="EXE_PATH" value="$PROJECT_DIR$/MindWork AI Studio/bin/Debug/net10.0/osx-arm64/mindworkAIStudio" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/MindWork AI Studio" />
<option name="PASS_PARENT_ENVS" value="1" />
@@ -18,7 +18,7 @@
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net9.0" />
<option name="PROJECT_TFM" value="net10.0" />
<method v="2">
<option name="Build" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Collect I18N content" run_configuration_type="ShConfigurationType" />
+2 -2
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Build</RootNamespace>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@@ -14,7 +14,7 @@
<PackageReference Include="Cocona" Version="2.2.0" />
<!-- Pins Cocona's transitive Microsoft.Extensions.Hosting 6.0.0, which pulled in the vulnerable System.Text.Json 6.0.0 (GHSA-8g4q-xg66-9fp4) -->
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.22" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.14" />
</ItemGroup>
<ItemGroup>
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Build.Tools;
public static class Environment
{
public const string DOTNET_VERSION = "net9.0";
public const string DOTNET_VERSION = "net10.0";
public const string BUILD_OFFLINE_ENVIRONMENT_VARIABLE = "AI_STUDIO_BUILD_OFFLINE";
public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false);
-54
View File
@@ -1,54 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build Script", "Build\Build Script.csproj", "{447A5590-68E1-4EF8-9451-A41AF5FBE571}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedTools", "SharedTools\SharedTools.csproj", "{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratedMappings", "SourceGeneratedMappings\SourceGeneratedMappings.csproj", "{4D7141D5-9C22-4D85-B748-290D15FF484C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{CD46329B-D135-4594-9A70-55D3480F8FEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrimSmokeTest", "TrimSmokeTest\TrimSmokeTest.csproj", "{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.Build.0 = Release|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.Build.0 = Debug|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.ActiveCfg = Release|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.Build.0 = Release|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.Build.0 = Release|Any CPU
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.Build.0 = Release|Any CPU
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD46329B-D135-4594-9A70-55D3480F8FEE}.Release|Any CPU.Build.0 = Release|Any CPU
{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F4B3B3C-E8C9-4CDC-A247-5029D777DFC2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
EndGlobal
+9
View File
@@ -0,0 +1,9 @@
<Solution>
<Project Path="Build/Build Script.csproj" />
<Project Path="MindWork AI Studio/MindWork AI Studio.csproj" />
<Project Path="SharedTools/SharedTools.csproj" />
<Project Path="SourceCodeRules/SourceCodeRules/SourceCodeRules.csproj" />
<Project Path="SourceGeneratedMappings/SourceGeneratedMappings.csproj" />
<Project Path="Tests/Tests.csproj" />
<Project Path="TrimSmokeTest/TrimSmokeTest.csproj" />
</Solution>
@@ -12301,9 +12301,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request
-- The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1133173666"] = "The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region."
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"
-- Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1856278860"] = "Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"
@@ -5,7 +5,7 @@
<PropertyGroup>
<Authors>Thorsten Sommer</Authors>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>CS8600;CS8602;CS8603;CS9057</WarningsAsErrors> <!-- CS9057: fail instead of silently skipping an analyzer which needs a newer compiler than the SDK's -->
<ImplicitUsings>enable</ImplicitUsings>
@@ -45,6 +45,13 @@
-->
<NoWarn>IL2026, CS8974</NoWarn>
<LangVersion>latest</LangVersion>
<!--
Since .NET 10, the scripts of Blazor come from a package, which the release embeds: see the
EmbedBlazorScript target. The SDK adds that package by itself when it spots Razor files; we
ask for it here, so that the release does not hinge on that guess.
-->
<RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>
</PropertyGroup>
<Import Project="..\Trimming.props" />
@@ -71,22 +78,20 @@
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
<PackageReference Include="MailKit" Version="4.18.2" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.22" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.22" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="10.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.14" />
<!--
Its assembly ships with ASP.NET Core, but the package also brings the build logic which writes the
manifest of the embedded files. Without that manifest, the release serves no static files at all, and
WebRootFiles finds neither the changelogs nor the ERI specifications in any build.
-->
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.14" />
<PackageReference Include="MudBlazor" Version="9.12.0" />
<PackageReference Include="MudBlazor.Markdown" Version="9.0.0" />
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
<PackageReference Include="LuaCSharp" Version="0.5.8" />
<!--
Not used by AI Studio directly: MailKit pulls it in through MimeKit, which asks for 10.0.0. That
version is affected by CVE-2026-70162 and CVE-2026-84008 (GHSA-2fmv-rv2c-59x7, GHSA-846f-f764-76mx),
fixed in 10.0.13. The package wins over the older copy within the .NET runtime, so upgrading .NET
alone does not help. Remove this reference once MimeKit itself asks for 10.0.13 or newer.
-->
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
</ItemGroup>
<ItemGroup>
@@ -117,13 +122,26 @@
<ReadLinesFromFile File="../../metadata.txt">
<Output TaskParameter="Lines" PropertyName="Metadata" />
</ReadLinesFromFile>
<!--
metadata.txt names the .NET SDK which the pipeline pins, but a local build may use another
one. Hence, we take the SDK which builds right now. Its .version file starts with the commit
it was built from. The .NET runtime is not recorded at all: the app asks for it while it runs.
-->
<PropertyGroup>
<DotnetSdkVersionFile>$(NetCoreRoot)sdk/$(NETCoreSdkVersion)/.version</DotnetSdkVersionFile>
</PropertyGroup>
<ReadLinesFromFile File="$(DotnetSdkVersionFile)" Condition="Exists('$(DotnetSdkVersionFile)')">
<Output TaskParameter="Lines" PropertyName="DotnetSdkVersionLines" />
</ReadLinesFromFile>
<PropertyGroup>
<MetaVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 0 ])</MetaVersion>
<MetaBuildTime>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 1 ])</MetaBuildTime>
<MetaBuild>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 2 ])</MetaBuild>
<MetaDotnetSdkVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 3 ])</MetaDotnetSdkVersion>
<MetaDotnetVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 4 ])</MetaDotnetVersion>
<MetaDotnetSdkVersion Condition="'$(DotnetSdkVersionLines)' != ''">$(NETCoreSdkVersion) (commit $([System.String]::Copy( $(DotnetSdkVersionLines) ).Split( ';' )[ 0 ].Substring( 0, 10 )))</MetaDotnetSdkVersion>
<MetaRustVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 5 ])</MetaRustVersion>
<MetaMudBlazorVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 6 ])</MetaMudBlazorVersion>
<MetaTauriVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 7 ])</MetaTauriVersion>
@@ -152,11 +170,10 @@
<_Parameter3>$(MetaBuild)</_Parameter3>
<_Parameter3_TypeName>System.UInt32</_Parameter3_TypeName>
<_Parameter4>$(MetaDotnetSdkVersion)</_Parameter4>
<_Parameter5>$(MetaDotnetVersion)</_Parameter5>
<_Parameter6>$(MetaRustVersion)</_Parameter6>
<_Parameter7>$(MetaMudBlazorVersion)</_Parameter7>
<_Parameter8>$(MetaTauriVersion)</_Parameter8>
<_Parameter9>$(MetaAppCommitHash)</_Parameter9>
<_Parameter5>$(MetaRustVersion)</_Parameter5>
<_Parameter6>$(MetaMudBlazorVersion)</_Parameter6>
<_Parameter7>$(MetaTauriVersion)</_Parameter7>
<_Parameter8>$(MetaAppCommitHash)</_Parameter8>
</AssemblyAttribute>
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataArchitecture">
<_Parameter1>$(MetaArchitecture)</_Parameter1>
@@ -171,6 +188,18 @@
</Target>
<!--
Since .NET 10, NuGet prunes a package whose assemblies ship with the shared framework, and with it the
package's build logic. Microsoft.Extensions.FileProviders.Embedded must not go this way, see its reference
above. .NET 10.0.0 already leaves it out of the pruning (dotnet/aspnetcore#63719); this target keeps it
even if a later version of .NET prunes it again. It is the workaround Microsoft gives in that issue.
-->
<Target Name="_PreserveFileProvidersEmbeddedPackageReference" AfterTargets="AddPrunePackageReferences">
<ItemGroup>
<PrunePackageReference Remove="Microsoft.Extensions.FileProviders.Embedded" />
</ItemGroup>
</Target>
<!--
Scoped CSS (`.razor.css`) is bundled into `$(AssemblyName).styles.css`, which App.razor links.
That bundle is generated at build time, so it never lives in the source tree's wwwroot. The
@@ -197,4 +226,28 @@
</ItemGroup>
</Target>
<!--
`_framework/blazor.web.js` makes the UI interactive, and App.razor loads it. Up to .NET 9, the shared
framework served it. Since .NET 10, it is a static web asset of the package Microsoft.AspNetCore.App.Internal.Assets,
which names its folder in BlazorFrameworkStaticWebAssetRoot. Like the scoped CSS bundle above, the release
would therefore lack it: the window would show the app, but nothing in it would react. Hence: embed it
before the manifest is written.
-->
<Target Name="EmbedBlazorScript" BeforeTargets="_CalculateEmbeddedFilesManifestInputs">
<PropertyGroup>
<BlazorScriptToEmbed>$(BlazorFrameworkStaticWebAssetRoot)\blazor.web.js</BlazorScriptToEmbed>
</PropertyGroup>
<!--
Fail loudly instead of shipping a release without the script: a missing file here means the
package moved it, or the SDK no longer adds the package.
-->
<Error Condition="'$(BlazorFrameworkStaticWebAssetRoot)' == '' Or !Exists('$(BlazorScriptToEmbed)')"
Text="The script of Blazor was not found at '$(BlazorScriptToEmbed)'. Without it, the release starts with a window which does not react at all. Check where .NET keeps blazor.web.js and adjust the EmbedBlazorScript target." />
<ItemGroup>
<EmbeddedResource Include="$(BlazorScriptToEmbed)" Link="wwwroot\_framework\blazor.web.js" />
</ItemGroup>
</Target>
</Project>
@@ -89,7 +89,7 @@ public partial class Information : MSGComponentBase
private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{DotnetRuntimeVersion.Current}";
private string VersionDotnetSdk => $"{T("Used .NET SDK")}: v{META_DATA.DotnetSdkVersion}";
@@ -12303,9 +12303,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "Die Anfrage
-- The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1133173666"] = "Der Anbieter „{0}“ hat die Anfrage abgelehnt. Möglicherweise ist Ihr Konto nicht für die Nutzung des ausgewählten Modells berechtigt, oder der Anbieter ist in Ihrer Region nicht verfügbar."
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Beim Versuch, die Antwort des LLM-Anbieters „{0}“ zu streamen, sind Probleme aufgetreten. Die Meldung lautet: „{1}“"
-- Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1856278860"] = "Der Versuch, die Antwort des LLM-Anbieters „{0}“ zu streamen, ist fehlgeschlagen. Der Stream konnte nicht gelesen werden. Die Meldung lautet: „{1}“"
@@ -12303,9 +12303,6 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request
-- The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region.
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1133173666"] = "The provider '{0}' refused the request. Your account might not be allowed to use the selected model, or the provider might not serve your region."
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"
-- Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1856278860"] = "Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"
@@ -1007,22 +1007,12 @@ public abstract class BaseProvider : IProvider, ISecretId
try
{
//
// Read the stream, line by line:
// Read the stream, line by line, until it ends. We never ask the reader whether the
// stream ended: that would wait for the next data without any timeout, and it would
// hold a thread meanwhile. Reading the next line tells the same, see below.
//
while (true)
{
try
{
if(streamReader.EndOfStream)
break;
}
catch (Exception e)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
this.logger.LogWarning(e, "Failed to read the end-of-stream state from {ProviderName} '{ProviderInstanceName}': {ErrorMessage}", providerName, this.InstanceName, e.Message);
break;
}
// Check if the token is canceled:
if (token.IsCancellationRequested)
{
@@ -1058,13 +1048,14 @@ public abstract class BaseProvider : IProvider, ISecretId
break;
}
// The stream ended:
if (line is null)
break;
// Skip empty lines:
if (string.IsNullOrWhiteSpace(line))
continue;
if (this.TryCreateProviderRequestExceptionFromStreamLine(providerName, line, out var providerRequestException))
throw providerRequestException;
+5 -6
View File
@@ -39,12 +39,11 @@ public sealed class Encryption(ILogger<Encryption> logger, byte[] secretPassword
// Derive key and iv vector: the operations take several seconds. Thus, using a task:
await Task.Run(() =>
{
using var keyVectorObj = new Rfc2898DeriveBytes(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512);
var keyBytes = keyVectorObj.GetBytes(32); // the max valid key length = 256 bit = 32 bytes
var ivBytes = keyVectorObj.GetBytes(16); // the only valid block size = 128 bit = 16 bytes
Array.Copy(keyBytes, this.key, this.key.Length);
Array.Copy(ivBytes, this.iv, this.iv.Length);
// The max valid key length = 256 bit = 32 bytes, and the only valid block size = 128 bit = 16 bytes:
var keyVectorBytes = Rfc2898DeriveBytes.Pbkdf2(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512, this.key.Length + this.iv.Length);
Array.Copy(keyVectorBytes, 0, this.key, 0, this.key.Length);
Array.Copy(keyVectorBytes, this.key.Length, this.iv, 0, this.iv.Length);
});
var initDuration = stopwatch.Elapsed;
@@ -292,11 +292,11 @@ public sealed class EnterpriseEncryption
/// </summary>
private Aes CreateAesV1(byte[] salt)
{
// Derive key and IV using PBKDF2:
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, V1_ITERATIONS, HashAlgorithmName.SHA512);
// Derive key and IV using PBKDF2, the first 32 bytes for AES-256 and the last 16 for its block size:
var keyAndIv = Rfc2898DeriveBytes.Pbkdf2(this.secretKey!, salt, V1_ITERATIONS, HashAlgorithmName.SHA512, 48);
var aes = Aes.Create();
aes.Key = keyDerivation.GetBytes(32); // AES-256
aes.IV = keyDerivation.GetBytes(16); // AES block size
aes.Key = keyAndIv[..32];
aes.IV = keyAndIv[32..];
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
return aes;
@@ -440,7 +440,7 @@ public static class ExternalHttpClientTimeout
customChain.ChainPolicy.CustomTrustStore.AddRange(customRootCertificateCache.Certificates);
customChain.ChainPolicy.ApplicationPolicy.Add(new Oid(TLS_SERVER_AUTHENTICATION_EKU_OID));
// Match the .NET 9 HttpClient default used for the initial system-trust validation.
// Match the HttpClient default used for the initial system-trust validation, which .NET 10 kept.
// Hostname, signature, validity, EKU, and root trust checks remain enabled.
customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
@@ -0,0 +1,41 @@
using System.Reflection;
namespace AIStudio.Tools.Metadata;
/// <summary>
/// The .NET runtime which runs the app.
/// </summary>
/// <remarks>
/// Asked while the app runs instead of taken from metadata.txt: a release brings its own runtime,
/// and a development build runs on whichever runtime the developer installed.
/// </remarks>
public static class DotnetRuntimeVersion
{
/// <summary>
/// How many characters of the commit we show, as metadata.txt does.
/// </summary>
private const int COMMIT_LENGTH = 10;
/// <summary>
/// Gets the version of the runtime together with the commit it was built from, such as
/// <c>10.0.14 (commit 0427b9abbe)</c>.
/// </summary>
public static string Current { get; } = Describe(Environment.Version, typeof(object).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion);
/// <summary>
/// Describes a version of the runtime together with the commit which the informational version
/// of its core library names after a plus sign, such as <c>10.0.14+0427b9abbead3f…</c>.
/// </summary>
/// <param name="version">The version of the runtime.</param>
/// <param name="informationalVersion">The informational version of the core library, if any.</param>
/// <returns>The version with its commit, or the version alone when the commit is unknown.</returns>
internal static string Describe(Version version, string? informationalVersion)
{
if (string.IsNullOrWhiteSpace(informationalVersion))
return version.ToString();
var plusIndex = informationalVersion.IndexOf('+');
var commit = plusIndex < 0 ? string.Empty : informationalVersion[(plusIndex + 1)..];
return commit.Length < COMMIT_LENGTH ? version.ToString() : $"{version} (commit {commit[..COMMIT_LENGTH]})";
}
}
@@ -7,7 +7,6 @@ public class MetaDataAttribute(
string buildTime,
uint buildNum,
string dotnetSdkVersion,
string dotnetVersion,
string rustVersion,
string mudBlazorVersion,
string tauriVersion,
@@ -20,8 +19,9 @@ public class MetaDataAttribute(
public uint BuildNum => buildNum;
public string DotnetVersion => dotnetVersion;
/// <summary>
/// Gets the .NET SDK which built the app. The .NET runtime which runs it is <see cref="DotnetRuntimeVersion.Current"/>.
/// </summary>
public string DotnetSdkVersion => dotnetSdkVersion;
public string RustVersion => rustVersion;
@@ -549,12 +549,22 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
}
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
public override Task StartAsync(CancellationToken cancellationToken)
{
// Changes to the embedding settings which do not pass through the queue, cf. ProcessMessage:
//
// Changes to the embedding settings which do not pass through the queue, cf. ProcessMessage.
// We listen for them while the app starts: since .NET 10, ExecuteAsync runs as a task of its
// own from its very first line, and a message sent before that task got its turn would go
// unnoticed.
//
MessageBus.INSTANCE.RegisterComponent(this);
this.ApplyFilters([], [Event.PLUGINS_RELOADED, Event.CONFIGURATION_CHANGED]);
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await this.WaitForInitialSettingsAndBootstrapAsync(stoppingToken);
while (!stoppingToken.IsCancellationRequested)
@@ -27,12 +27,9 @@ public partial class RustService
// Read events line by line:
using var reader = new StreamReader(stream);
// Read until the end of the stream or cancellation:
while(!reader.EndOfStream && !stopToken.IsCancellationRequested)
// Read the next line of JSON until the end of the stream or cancellation:
while(!stopToken.IsCancellationRequested && await reader.ReadLineAsync(stopToken) is { } line)
{
// Read the next line of JSON from the stream:
var line = await reader.ReadLineAsync(stopToken);
// Skip empty lines:
if (string.IsNullOrWhiteSpace(line))
continue;
@@ -352,9 +352,8 @@ public sealed partial class RustService
await using var stream = await response.Content.ReadAsStreamAsync(token);
using var reader = new StreamReader(stream);
while (!reader.EndOfStream && !token.IsCancellationRequested)
while (!token.IsCancellationRequested && await reader.ReadLineAsync(token) is { } line)
{
var line = await reader.ReadLineAsync(token);
if (string.IsNullOrWhiteSpace(line))
continue;
+85 -302
View File
@@ -1,7 +1,7 @@
{
"version": 1,
"dependencies": {
"net9.0": {
"net10.0": {
"CodeBeam.MudBlazor.Extensions": {
"type": "Direct",
"requested": "[9.2.0, )",
@@ -42,60 +42,53 @@
"resolved": "4.18.2",
"contentHash": "ofqddpSbdrD00NqTD0ZyVfKvrl/ZZ70VFFEQ+DuR8r8CdbY3U02/sdb0CSQBNUqJifsXfVHIwRe1ToR2MvJJkw==",
"dependencies": {
"MimeKit": "4.18.2",
"System.Formats.Asn1": "10.0.13"
"MimeKit": "4.18.2"
}
},
"Microsoft.AspNetCore.App.Internal.Assets": {
"type": "Direct",
"requested": "[10.0.14, )",
"resolved": "10.0.14",
"contentHash": "T1IOYbID1msKjQEh684HebnN+Xnq/pocMXDukOMZtzzPTf9c4/30+/JndM/1txrqwuSYqWvLiAQWzu4U93cshw=="
},
"Microsoft.Data.Sqlite.Core": {
"type": "Direct",
"requested": "[9.0.22, )",
"resolved": "9.0.22",
"contentHash": "ivq0VMiRaR2tvwcZYbzJUMrQtbBSLDkZyTNM8FsBv56Ne664XZR2XlhDF0RuAsCDt5lU9yKRs0WHOuDbROv2JQ==",
"requested": "[10.0.14, )",
"resolved": "10.0.14",
"contentHash": "1p9rO3Q2X/L/9YrYog3vaDzi8u+WZfJCOkDYoHqmofqYP0DLPLPyZcS2Rnh/79JXAodgbs1AgsaNwLpIvC3Xew==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.13"
}
},
"Microsoft.EntityFrameworkCore.Sqlite": {
"type": "Direct",
"requested": "[9.0.22, )",
"resolved": "9.0.22",
"contentHash": "yj54/8fLJC0uI7t5m7uUid3nOYDl7J+G3Hf6MSz+ULyeguA1fxZw3F1u3wMnEBEaH0i8d2hCAMdxanDplIUfvw==",
"requested": "[10.0.14, )",
"resolved": "10.0.14",
"contentHash": "lxrwnVwvlIfQD4IF4WUVupE6Qxfj3MfFftDVwo7tsGAWCKLL8Ttb8UjIt+gE4e9nRlH7EgdC6TrsArFGnNYREw==",
"dependencies": {
"Microsoft.EntityFrameworkCore.Sqlite.Core": "9.0.22",
"Microsoft.Extensions.Caching.Memory": "9.0.22",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.22",
"Microsoft.Extensions.DependencyModel": "9.0.22",
"Microsoft.Extensions.Logging": "9.0.22",
"Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.14",
"Microsoft.Extensions.DependencyModel": "10.0.14",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.13",
"SQLitePCLRaw.core": "2.1.13",
"System.Text.Json": "9.0.22"
"SQLitePCLRaw.core": "2.1.13"
}
},
"Microsoft.Extensions.FileProviders.Embedded": {
"type": "Direct",
"requested": "[9.0.22, )",
"resolved": "9.0.22",
"contentHash": "8pjPIPzG5OGIBG65TSUExMZeMeFpCDVJPtNzYtcJyNPCqgOI/skfKq5ZEca1VmswL2CQ7oxEYiCFNO2LhAJ4uA==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.22"
}
"requested": "[10.0.14, )",
"resolved": "10.0.14",
"contentHash": "v+KNMXj2RktQ690ijlFYniH8gggO+EVMjCZSou+NcpI2mKNAT27Dgn2UfiJKbwQ84v2Avp6Mwceqog8gvQn5Cw=="
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[9.0.22, )",
"resolved": "9.0.22",
"contentHash": "S61AsICGqZii54jXMY56s6xvu3PcIe9VPeBxcTAWwNt5P21l2Arcy36XfATNvZAEqoAH7OfGNzng+aoXkqhsxQ=="
"requested": "[10.0.14, )",
"resolved": "10.0.14",
"contentHash": "YlTamXppEQls6DYWt7RdUuwWDb0lyw/vXCaGeQpLCpsLmfwGuCkOz/vIGgMTV7ECsJcavkCOkxZXv9s8rdAHkQ=="
},
"MudBlazor": {
"type": "Direct",
"requested": "[9.12.0, )",
"resolved": "9.12.0",
"contentHash": "T6PQUCJe60UTSy3u1F8R2nuC06Cox6WEhon5GA2/AZNVI1WstlPSJZdGPyp6pHFpWdjPpla65piLpyJsbW+1rA==",
"dependencies": {
"Microsoft.AspNetCore.Components": "9.0.1",
"Microsoft.AspNetCore.Components.Web": "9.0.1",
"Microsoft.Extensions.Localization": "9.0.1"
}
"contentHash": "T6PQUCJe60UTSy3u1F8R2nuC06Cox6WEhon5GA2/AZNVI1WstlPSJZdGPyp6pHFpWdjPpla65piLpyJsbW+1rA=="
},
"MudBlazor.Markdown": {
"type": "Direct",
@@ -126,16 +119,6 @@
"SQLitePCLRaw.config.e_sqlite3": "3.0.5"
}
},
"System.Security.Cryptography.Pkcs": {
"type": "Direct",
"requested": "[10.0.13, )",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.13",
"System.Formats.Asn1": "10.0.13"
}
},
"BouncyCastle.Cryptography": {
"type": "Transitive",
"resolved": "2.7.0",
@@ -146,7 +129,7 @@
"resolved": "3.5.1",
"contentHash": "U5txtc3ORno73xQx9Lf2gWzfaSZnZwKHfLkTAslhlew9lxe5XbUiCt0dY1fHeAf8yRqszUAe5i/+xLC9R/Xfsw==",
"dependencies": {
"System.IO.Packaging": "8.0.1"
"System.IO.Packaging": "10.0.2"
}
},
"LuaCSharp.Annotations": {
@@ -164,213 +147,48 @@
"resolved": "0.45.0",
"contentHash": "ObNLcA1b+0lpNNoEg256g9faMeJZi35wZW0AnKJ4nGPJe+5qkwnV26kUvQTHuanFnSX9SdvPzOO41BVJ6XarAg=="
},
"Microsoft.AspNetCore.Authorization": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "WgLlLBlMczb2+QLNG6sM95OUZ0EBztz60k/N75tjIgpyu0SdpIfYytAmX/7JJAjRTZF0c/CrWaQV+SH9FuGsrA==",
"dependencies": {
"Microsoft.AspNetCore.Metadata": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Microsoft.Extensions.Options": "9.0.1"
}
},
"Microsoft.AspNetCore.Components": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "6pwfbQKNtvPkbF4tCGiAKGyt6BVpu58xAXz7u2YXcUKTNmNxrymbG1mEyMc0EPzVdnquDDqTyfXM3mC1EJycxQ==",
"dependencies": {
"Microsoft.AspNetCore.Authorization": "9.0.1",
"Microsoft.AspNetCore.Components.Analyzers": "9.0.1"
}
},
"Microsoft.AspNetCore.Components.Analyzers": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "I8Rs4LXT5UQxM5Nin2+Oj8aSY2heszSZ3EyTLgt3mxmfiRPrVO7D8NNSsf1voI2Gb0qFJceof/J5c9E+nfNuHw=="
},
"Microsoft.AspNetCore.Components.Forms": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "KyULVU32bLz74LWDwPEwNUEllTehzWJuM7YAsz80rMKEzvR0K8cRjRzO0fnN/nfydMeLRRlbI0xj8wnEAymLVw==",
"dependencies": {
"Microsoft.AspNetCore.Components": "9.0.1"
}
},
"Microsoft.AspNetCore.Components.Web": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "LI0vjYEd9MaDZPDQxPCn4gGYDkEC5U9rp1nWZo7rPozJxgTG2zU3WERujxTi2LeAC2ZzdXlOVCrUyPQ55LZV2A==",
"dependencies": {
"Microsoft.AspNetCore.Components": "9.0.1",
"Microsoft.AspNetCore.Components.Forms": "9.0.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.Primitives": "9.0.1",
"Microsoft.JSInterop": "9.0.1"
}
},
"Microsoft.AspNetCore.Metadata": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "EZnHifamF7IFEIyjAKMtJM3I/94OIe72i3P09v5oL0twmsmfQwal6Ni3m8lbB5mge3jWFhMozeW+rUdRSqnXRQ=="
},
"Microsoft.Bcl.Cryptography": {
"type": "Transitive",
"resolved": "10.0.13",
"contentHash": "6H3QqvjxTgCt4gxVVCJVjo35Zh/gzjw7NXidIx0ghZrgYYMUja/7JJ4g39ynE/OH5YBBbUhlmirGR/AuYpNIuA=="
},
"Microsoft.EntityFrameworkCore": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "wBYt52v8cQ5abaA8jR3m+LPV8N3ymhz0pTPMvwYck9VZ9ItcYIu130qEYdsinMNt9co+4n7SMZdI1dFjJHIvNQ==",
"resolved": "10.0.14",
"contentHash": "vY3931PYPIDBbKovGHbACmpOWvURvKNyRXfCjMh5yLW+FH6vLWeqUu2xzSCRo4Oz4tgx9GYpd0JlV5LDyAoQdg==",
"dependencies": {
"Microsoft.EntityFrameworkCore.Abstractions": "9.0.22",
"Microsoft.EntityFrameworkCore.Analyzers": "9.0.22",
"Microsoft.Extensions.Caching.Memory": "9.0.22",
"Microsoft.Extensions.Logging": "9.0.22"
"Microsoft.EntityFrameworkCore.Abstractions": "10.0.14",
"Microsoft.EntityFrameworkCore.Analyzers": "10.0.14"
}
},
"Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "H1OiSYqSi4q9LnYfr4H6qkMOiS+bMSylLkbTcjlotI0aRojjDPHS+P2JAIn+kvzd8+wcVbzD1Jw0JexBAjiK2Q=="
"resolved": "10.0.14",
"contentHash": "/SaznoTTFfKzxYEbCPAwwKBR3XT92DZgH4jJ3/cqs2skP3o2D+TL6i9Aaj+8Azwtu6/sRd0ve4P2mN12nfVmCw=="
},
"Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "wtYr03ZnX3a7x92WE0yELwyhN2EooJ7BTlY4N2gBPaVBsg2XwjEKyyGBLbkhc9fgKaQ6ed1fEgnHmZ9YCfTlLQ=="
"resolved": "10.0.14",
"contentHash": "gaANlGNTxkgODX2eeVT8jjd3AmlFilQV4DFf8qoW30+wUCm2o4N7nEHtZvnHIwfhaJni2DqcjE47Au7tC0Q5jA=="
},
"Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "BwTl8180y2ct3po1+5MYk72HY0qZsbB4nqj/bbaHi9stz23Sxsf3xcG0CtASjm0T5UzBPVLAj9CNinuWfiLx3Q==",
"resolved": "10.0.14",
"contentHash": "Ut0kgtfoKX9QElQVEXSEAgdQc79jISWStVWSOhfPDDHNSKMwFzSXG3gtdYFTAbsBoV+8NtXdVaj4xwdqeky8UA==",
"dependencies": {
"Microsoft.EntityFrameworkCore": "9.0.22",
"Microsoft.Extensions.Caching.Memory": "9.0.22",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.22",
"Microsoft.Extensions.Logging": "9.0.22"
"Microsoft.EntityFrameworkCore": "10.0.14"
}
},
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "AIvmI99uWB47b82svKMcevpNLhX+2VBPf/l9VJUGP+8PLBle8jwPguGwzy3MdjsM2AfVE/QoJK7mQ2Xd8e0vJw==",
"resolved": "10.0.14",
"contentHash": "08VGlMQ9WbK869as9Ho+VZQbM5Qu6zdorgeh6ShVwQgLYgfUBiaIm02T7spfW7Xc1MBHCvclwPgGWpvx5BYGXQ==",
"dependencies": {
"Microsoft.Data.Sqlite.Core": "9.0.22",
"Microsoft.EntityFrameworkCore.Relational": "9.0.22",
"Microsoft.Extensions.Caching.Memory": "9.0.22",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.22",
"Microsoft.Extensions.DependencyModel": "9.0.22",
"Microsoft.Extensions.Logging": "9.0.22",
"SQLitePCLRaw.core": "2.1.13",
"System.Text.Json": "9.0.22"
"Microsoft.Data.Sqlite.Core": "10.0.14",
"Microsoft.EntityFrameworkCore.Relational": "10.0.14",
"Microsoft.Extensions.DependencyModel": "10.0.14",
"SQLitePCLRaw.core": "2.1.13"
}
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "hQo0hI5IDADpxo3wiGJa6A2T5InBCWPAY65J2QZAPoy+5e9MDgpRGERei7DABAPd58hnjwnpFVXa/NUjcygqmw==",
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.22"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "V6Kzo5peqxfE5Dwsgt9ngK5hnJwI2F/QXqh2HR2naUCx1j8sGEs/wBhUib83PiaRi5Oc8Iux1nreLVwmfResDA==",
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "9.0.22",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.22",
"Microsoft.Extensions.Logging.Abstractions": "9.0.22",
"Microsoft.Extensions.Options": "9.0.22",
"Microsoft.Extensions.Primitives": "9.0.22"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "JTG92S+wiC49A3edjZTOk5/HMaFqaMn2/Y3RAAt9tK0Aoekfvp6ETJz3wOVsRCFqmAPX84vgcKtu/y920WBAGA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.22"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "fG8YKmh9xZbVSaKEHDQfG/BEToPezfIw9/EfIpIRVGVftJyFpp7KNqaiZmKDm2apBfjRA0iSdeyxNS9Kc842Dw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.22"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "SFzDXodpxwCTJQ9IsmqhCsh8fAElRqGtuOqwQACA7pKBj277g+A8q8zzZebk0Q+MKrHX+WFT3/i64n4BdhsMpQ=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "sqIODKUAUaj/im0LcKSW59JY74vMnxIVDFG61ASH6O4q5LaazjP2XRDqOzjou5bjhI1AW4F7oyOz01NsmH194A=="
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "HhBR/vC5PwIEkvNqOcBR0+VRYBXR5zsv8Ddsk4qj9Jh29bE+LCxd1cPx7rdK0PIANVT82vAmJcXUT7eFw7CwbA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.22"
}
},
"Microsoft.Extensions.Localization": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "UgvX4Yb2T3tEsKT30ktZr0H7kTRPapCgEH0bdTwxiEGSdA39/hAQMvvb+vgHpqmevDU5+puyI9ujRkmmbF946w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Localization.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Microsoft.Extensions.Options": "9.0.1"
}
},
"Microsoft.Extensions.Localization.Abstractions": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "CABog43lyaZQMjmlktuImCy6zmAzRBaXqN81uPaMQjlp//ISDVYItZPh6KWpWRF4MY/B67X5oDc3JTUpfdocZw=="
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "mYX+XnqcOr1Mffohw3y91BXlLV0df848jIbT2e/h4GZ5uQBJaBdo7VuX5mWJdnpu7roTUUUMYHjYR9u/O/XRqg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.22",
"Microsoft.Extensions.Logging.Abstractions": "9.0.22",
"Microsoft.Extensions.Options": "9.0.22"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "REayp367aujZONmJbX6ctXSZAJjgovhhIy8qoHsY50y/nXhtpPPndsXgz7EHnbZef5eCIMG0VbTjFf3/sXs5aQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.22"
}
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "HXvtEnW4d3LdZuA2T31tT7DMkhtXOGvncxNN6cqoQLV/HDtF/TYTW/8hVy+di/w/VerPOJ5c03nhfjxkHORwiw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.22",
"Microsoft.Extensions.Primitives": "9.0.22"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "sna268GZylvTK/y/HJgjs1fqKBDG3Yj4nsCQQNiIhKKITlvcFbEaf65FTC754VXHu1PE1HoDvXblRwi7LU8CGw=="
},
"Microsoft.JSInterop": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "/xBwIfb0YoC2Muv6EsHjxpqZw2aKv94+i0g0FWZvqvGv3DeAy+8wipAuECVvKYEs2EIclRD41bjajHLoD6mTtw=="
"resolved": "10.0.14",
"contentHash": "N8OcqM0370wSXMJ3ZxO66rSElHrIgxvpfUn0vV7zRj7NvKo0nfwgEoTmC3Yz3860o60IaQ4KIPyeLIGX9EeNWA=="
},
"MimeKit": {
"type": "Transitive",
@@ -407,125 +225,90 @@
"SQLitePCLRaw.core": "3.0.5"
}
},
"System.Formats.Asn1": {
"type": "Transitive",
"resolved": "10.0.13",
"contentHash": "uwKFGyJaUQ8xsf+wddv512+UIU1O4reOveom3y0gpQr86P1ReAKcQa/UdOZtg1etTIHgb01/grXp00tgeGBxqA=="
},
"System.IO.Packaging": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw=="
"resolved": "10.0.2",
"contentHash": "JTpM4z0wpoIHHDvlCU27HsXo+zVnpWib94HXQpzzr+jc/P9NYf4w353AK4MXyGq/grm1mbLi7eXsOsDU8sGmNg=="
},
"System.Text.Json": {
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "9.0.22",
"contentHash": "bP1LqWP1h1/J/XO69m5kdLF7M1HT6YWpM7UflcIKLLTvJep8HoS6I/F8FnV/uGhqmStLHY8PuiQAmjQg+qqqcg=="
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw=="
},
"sharedtools": {
"type": "Project"
}
},
"net9.0/linux-arm64": {
"System.Security.Cryptography.Pkcs": {
"type": "Direct",
"requested": "[10.0.13, )",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.13",
"System.Formats.Asn1": "10.0.13"
}
},
"net10.0/linux-arm64": {
"SQLite": {
"type": "Transitive",
"resolved": "3.53.4",
"contentHash": "KN7jeWqgUPeBRe1FlcpZURzxomuKKEKHmBBQfg+Nx7NkY1LjKhzHvH+3ASkNvhayESE34nMBinL9CV21JfPRJw=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw=="
}
},
"net9.0/linux-x64": {
"System.Security.Cryptography.Pkcs": {
"type": "Direct",
"requested": "[10.0.13, )",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.13",
"System.Formats.Asn1": "10.0.13"
}
},
"net10.0/linux-x64": {
"SQLite": {
"type": "Transitive",
"resolved": "3.53.4",
"contentHash": "KN7jeWqgUPeBRe1FlcpZURzxomuKKEKHmBBQfg+Nx7NkY1LjKhzHvH+3ASkNvhayESE34nMBinL9CV21JfPRJw=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw=="
}
},
"net9.0/osx-arm64": {
"System.Security.Cryptography.Pkcs": {
"type": "Direct",
"requested": "[10.0.13, )",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.13",
"System.Formats.Asn1": "10.0.13"
}
},
"net10.0/osx-arm64": {
"SQLite": {
"type": "Transitive",
"resolved": "3.53.4",
"contentHash": "KN7jeWqgUPeBRe1FlcpZURzxomuKKEKHmBBQfg+Nx7NkY1LjKhzHvH+3ASkNvhayESE34nMBinL9CV21JfPRJw=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw=="
}
},
"net9.0/osx-x64": {
"System.Security.Cryptography.Pkcs": {
"type": "Direct",
"requested": "[10.0.13, )",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.13",
"System.Formats.Asn1": "10.0.13"
}
},
"net10.0/osx-x64": {
"SQLite": {
"type": "Transitive",
"resolved": "3.53.4",
"contentHash": "KN7jeWqgUPeBRe1FlcpZURzxomuKKEKHmBBQfg+Nx7NkY1LjKhzHvH+3ASkNvhayESE34nMBinL9CV21JfPRJw=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw=="
}
},
"net9.0/win-arm64": {
"System.Security.Cryptography.Pkcs": {
"type": "Direct",
"requested": "[10.0.13, )",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.13",
"System.Formats.Asn1": "10.0.13"
}
},
"net10.0/win-arm64": {
"SQLite": {
"type": "Transitive",
"resolved": "3.53.4",
"contentHash": "KN7jeWqgUPeBRe1FlcpZURzxomuKKEKHmBBQfg+Nx7NkY1LjKhzHvH+3ASkNvhayESE34nMBinL9CV21JfPRJw=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw=="
}
},
"net9.0/win-x64": {
"System.Security.Cryptography.Pkcs": {
"type": "Direct",
"requested": "[10.0.13, )",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw==",
"dependencies": {
"Microsoft.Bcl.Cryptography": "10.0.13",
"System.Formats.Asn1": "10.0.13"
}
},
"net10.0/win-x64": {
"SQLite": {
"type": "Transitive",
"resolved": "3.53.4",
"contentHash": "KN7jeWqgUPeBRe1FlcpZURzxomuKKEKHmBBQfg+Nx7NkY1LjKhzHvH+3ASkNvhayESE34nMBinL9CV21JfPRJw=="
},
"System.Security.Cryptography.Pkcs": {
"type": "Transitive",
"resolved": "10.0.13",
"contentHash": "a+EovgC1fBnwdlQ6cn3j4JFBfKqiFaFvoAhjisOoSvazLAcIznaBzPQP0oYnvccmeXfqUH1KebVZOxSeH6rMaw=="
}
}
}
@@ -15,4 +15,5 @@
- Fixed chats being stored although you chose "Store chats manually" or "Disable workspaces". AI Studio now stores a chat only when you click "Save chat", or not at all while workspaces are disabled. In both modes, leaving a chat whose answer is still being written asks you first and stops that answer. Chats outside of workspaces that earlier versions stored this way appear among your disappearing chats once you use workspaces again.
- Upgraded several libraries to improve security.
- Upgraded Tauri to v2.12.5.
- Upgraded MudBlazor to v9.12.0.
- Upgraded MudBlazor to v9.12.0.
- Upgraded to .NET v10.0.14.
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -20,13 +20,13 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!--
Never above the Roslyn of the .NET SDK we build with (see metadata.txt): the 9.0.1xx SDKs ship
Roslyn 4.12. Built against a newer one, the compiler skips these analyzers with CS9057. Raise
Never above the Roslyn of the .NET SDK we build with (see metadata.txt): the 10.0.1xx SDKs ship
Roslyn 5.0. Built against a newer one, the compiler skips these analyzers with CS9057. Raise
both only together with the SDK feature band, with SourceGeneratedMappings, and with the Tests
project.
-->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.12.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
</ItemGroup>
</Project>
@@ -21,11 +21,11 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!--
Never above the Roslyn of the .NET SDK we build with (see metadata.txt): the 9.0.1xx SDKs ship
Roslyn 4.12. Built against a newer one, the compiler skips this generator with CS9057, and the
Never above the Roslyn of the .NET SDK we build with (see metadata.txt): the 10.0.1xx SDKs ship
Roslyn 5.0. Built against a newer one, the compiler skips this generator with CS9057, and the
generated namespaces such as AIStudio.Tools.PluginSystem.Assistants.Icons go missing. Raise it
only together with the SDK feature band, with SourceCodeRules, and with the Tests project.
-->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
</ItemGroup>
</Project>
@@ -26,12 +26,12 @@ public sealed class MissingInterpolationAnalyzerTests
{
var reported = await AnalyzeAsync(expression);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(reported, Has.Count.EqualTo(1));
Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0014"));
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain("\"{this.location}\""));
});
}
}
[Test]
@@ -25,13 +25,13 @@ public sealed class BatchProcessingOutputFolderTests
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, PROJECT_FOLDER, RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "notes", "minutes.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "log.csv")), Is.False, "The log of a run lies in the output folder, so a file of this name in the input folder is a document.");
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True);
});
}
}
[Test]
@@ -39,12 +39,12 @@ public sealed class BatchProcessingOutputFolderTests
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, DESKTOP, RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "notes", "minutes.md")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True);
});
}
}
[Test]
@@ -53,13 +53,13 @@ public sealed class BatchProcessingOutputFolderTests
var resultsFolder = Path.Combine(INPUT_FOLDER, "ai-results");
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, resultsFolder, RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(resultsFolder, "report_result.md")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.Enters(resultsFolder), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True);
});
}
}
[Test]
@@ -69,14 +69,14 @@ public sealed class BatchProcessingOutputFolderTests
var resultsFolder = Path.Combine(parentFolder, "results");
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, resultsFolder, RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(resultsFolder, "report_result.md")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(parentFolder, "report.pdf")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "2026")), Is.True);
Assert.That(outputFolder.Enters(parentFolder), Is.True);
Assert.That(outputFolder.Enters(resultsFolder), Is.False);
});
}
}
[Test]
@@ -84,11 +84,11 @@ public sealed class BatchProcessingOutputFolderTests
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, Path.Combine(INPUT_FOLDER, "out"), RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "output-notes.md")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "output")), Is.True);
});
}
}
[Test]
@@ -96,12 +96,12 @@ public sealed class BatchProcessingOutputFolderTests
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, $"{INPUT_FOLDER}-results", RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "log.csv")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True);
});
}
}
[TestCase(false)]
@@ -111,13 +111,13 @@ public sealed class BatchProcessingOutputFolderTests
var outputDirectory = withTrailingSeparator ? INPUT_FOLDER + Path.DirectorySeparatorChar : INPUT_FOLDER;
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, outputDirectory, RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "log.csv")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, RESULTS_FILE_NAME)), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True, "Leaving out the output folder would leave out the input folder, too.");
});
}
}
[Test]
@@ -125,13 +125,13 @@ public sealed class BatchProcessingOutputFolderTests
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, INPUT_FOLDER, RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report_result.md")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report_result_2.docx")), Is.True, "A second result of the same name gets a counter.");
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "Report_Result.HTML")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.v2_result.md")), Is.True, "Only the last extension of the document makes way for the suffix.");
});
}
}
[Test]
@@ -139,14 +139,14 @@ public sealed class BatchProcessingOutputFolderTests
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, INPUT_FOLDER, RESULTS_FILE_NAME);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "result.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "my_results.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report_result_draft.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report_result_.md")), Is.False);
});
}
}
[Test]
@@ -155,11 +155,11 @@ public sealed class BatchProcessingOutputFolderTests
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, INPUT_FOLDER, RESULTS_FILE_NAME);
var subfolder = Path.Combine(INPUT_FOLDER, "notes");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(subfolder, "log.csv")), Is.False, "A run writes into the output folder itself, never into one of its subfolders.");
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(subfolder, RESULTS_FILE_NAME)), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(subfolder, "report_result.md")), Is.False);
});
}
}
}
@@ -32,11 +32,11 @@ public sealed class ERIServerFileListTests
var isRead = ERIServerFileList.TryParse(ANSWER, out var files);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(isRead, Is.True);
Assert.That(files, Is.EqualTo(FILES));
});
}
}
[Test]
@@ -44,11 +44,11 @@ public sealed class ERIServerFileListTests
{
var isRead = ERIServerFileList.TryParse("""{ "files": [ "src\\main.py" ] }""", out var files);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(isRead, Is.True);
Assert.That(files, Is.EqualTo(PATH_WITH_BACKSLASH));
});
}
}
[Test]
@@ -56,11 +56,11 @@ public sealed class ERIServerFileListTests
{
var isRead = ERIServerFileList.TryParse("""{ "files": [ "main.py", "", " " ] }""", out var files);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(isRead, Is.True);
Assert.That(files, Is.EqualTo(MAIN_PY));
});
}
}
[TestCase("""[ "main.py" ]""", TestName = "A list without its object")]
@@ -73,10 +73,10 @@ public sealed class ERIServerFileListTests
{
var isRead = ERIServerFileList.TryParse(answer, out var files);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(isRead, Is.False);
Assert.That(files, Is.Empty);
});
}
}
}
@@ -45,11 +45,11 @@ public sealed class ERIServerFileWritingTests
{
var outcome = await ERIServerFiles.WriteAsync(this.baseDirectory, "src/my-server/main.py", ANSWER, NullLogger.Instance);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outcome, Is.EqualTo(ERIServerFileOutcome.WRITTEN));
Assert.That(File.ReadAllText(Path.Join(this.baseDirectory, "src", "my-server", "main.py")), Is.EqualTo(MAIN_PY));
});
}
}
[Test]
@@ -58,11 +58,11 @@ public sealed class ERIServerFileWritingTests
var file = $"../outside-{Guid.NewGuid():N}.py";
var outcome = await ERIServerFiles.WriteAsync(this.baseDirectory, file, ANSWER, NullLogger.Instance);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outcome, Is.EqualTo(ERIServerFileOutcome.OUTSIDE_BASE_DIRECTORY));
Assert.That(File.Exists(Path.GetFullPath(Path.Join(this.baseDirectory, file))), Is.False);
});
}
}
[Test]
@@ -70,11 +70,11 @@ public sealed class ERIServerFileWritingTests
{
var outcome = await ERIServerFiles.WriteAsync(this.baseDirectory, "main.py", "## main.py\n\n```python\nprint(", NullLogger.Instance);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(outcome, Is.EqualTo(ERIServerFileOutcome.NO_COMPLETE_CODE_BLOCK));
Assert.That(File.Exists(Path.Join(this.baseDirectory, "main.py")), Is.False);
});
}
}
[Test]
@@ -51,11 +51,11 @@ public sealed class ERIServerPromptTests
{
var lines = Lines(new ERIServerPrompt { AuthenticationMethods = [Auth.NONE, Auth.USERNAME_PASSWORD], AuthDescription = AUTH_DESCRIPTION });
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(lines, Does.Contain("The authentication process is planned as follows:"));
Assert.That(lines, Does.Contain(AUTH_DESCRIPTION));
});
}
}
[Test]
@@ -36,7 +36,7 @@ public sealed class SwotAnalysisContentPresentationTests
var restoredContent = (ContentText)restoredThread!.Blocks.Single().Content!;
var presentationWasRead = SwotAnalysisContentPresentation.TryRead(restoredContent.Presentation, out var restoredResult);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(clone.Presentation, Is.Not.SameAs(content.Presentation));
Assert.That(clone.Text, Is.EqualTo(content.Text));
@@ -45,7 +45,7 @@ public sealed class SwotAnalysisContentPresentationTests
Assert.That(restoredResult.MatrixHeading, Is.EqualTo("SWOT Matrix"));
Assert.That(restoredResult.Strengths.Findings.Single().Summary, Is.EqualTo("Experienced team"));
Assert.That(restoredResult.PrioritizedActions.Items.Single().Action, Is.EqualTo("Prepare expansion"));
});
}
}
[Test]
@@ -59,11 +59,11 @@ public sealed class SwotAnalysisContentPresentationTests
var presentationWasRead = SwotAnalysisContentPresentation.TryRead(content.Presentation, out _);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(presentationWasRead, Is.False);
Assert.That(content.Text, Is.EqualTo("Fallback Markdown"));
});
}
}
[Test]
@@ -90,12 +90,12 @@ public sealed class SwotAnalysisContentPresentationTests
var presentationWasRead = SwotAnalysisContentPresentation.TryRead(content.Presentation, out var readResult);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(presentationWasRead, Is.True);
Assert.That(readResult.Categories.All(category => category.Findings.Count == 0), Is.True);
Assert.That(readResult.PrioritizedActions.Items, Is.Empty);
});
}
}
[Test]
@@ -16,7 +16,7 @@ public sealed class SwotAnalysisMarkdownFormatterTests
var markdown = SwotAnalysisMarkdownFormatter.Format(result);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(markdown, Does.Contain("| | Positive | Negative |"));
Assert.That(Count(markdown, "Experienced team"), Is.GreaterThanOrEqualTo(2));
@@ -26,7 +26,7 @@ public sealed class SwotAnalysisMarkdownFormatterTests
Assert.That(markdown.IndexOf("## SWOT Matrix", StringComparison.Ordinal), Is.LessThan(markdown.IndexOf("## Strengths", StringComparison.Ordinal)));
Assert.That(markdown.IndexOf("## SWOT Matrix", StringComparison.Ordinal), Is.LessThan(markdown.IndexOf("## Prioritized Actions", StringComparison.Ordinal)));
Assert.That(markdown, Does.Contain("| **External** | **Opportunities:** Growing market | **Threats:** New competitor |"));
});
}
}
[Test]
@@ -45,13 +45,13 @@ public sealed class SwotAnalysisMarkdownFormatterTests
.Select(heading => markdown.IndexOf(heading, StringComparison.Ordinal))
.ToList();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(positions, Is.All.GreaterThanOrEqualTo(0));
Assert.That(positions, Is.Ordered);
Assert.That(markdown, Does.Contain("The position is solid\\.\n\nThe competitor is the main risk\\."));
Assert.That(markdown, Does.EndWith("- **Moved the new competitor to the threats\\.** — It is external and negative\\."));
});
}
}
[Test]
@@ -13,7 +13,7 @@ public sealed class SwotAnalysisModerationTests
var parsed = SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(CreateModeration()), out var moderation, out var issue);
var result = SwotAnalysisModeration.Assemble(moderation);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parsed, Is.True);
Assert.That(issue, Is.Empty);
@@ -23,7 +23,7 @@ public sealed class SwotAnalysisModerationTests
Assert.That(result.OverallAssessment.Paragraphs, Is.EqualTo(new[] { "The position is solid.", "The competitor is the main risk." }));
Assert.That(result.ModerationNotes.Items.Single().Change, Is.EqualTo("Merged two strengths about the team."));
Assert.That(result.PrioritizedActions.Items.Single().AddressedFactors, Is.EqualTo(new[] { "Experienced team", "Growing market" }));
});
}
}
[Test]
@@ -34,11 +34,11 @@ public sealed class SwotAnalysisModerationTests
var parsed = SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out var parsedModeration, out _);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parsed, Is.True);
Assert.That(SwotAnalysisModeration.Assemble(parsedModeration).PrioritizedActions.Items.Single().AddressedFactors, Is.EqualTo(new[] { "Experienced team", "Growing market" }));
});
}
}
[Test]
@@ -47,11 +47,11 @@ public sealed class SwotAnalysisModerationTests
var moderation = CreateModeration();
moderation.Strengths.Findings.Single().FactorId = "W2";
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
Assert.That(issue, Is.EqualTo("Finding 1 of strengths has the factor_id \"W2\", which does not start with \"S\"."));
});
}
}
[Test]
@@ -60,11 +60,11 @@ public sealed class SwotAnalysisModerationTests
var moderation = CreateModeration();
moderation.Strengths.Findings.Add(Finding("s1", "Loyal customers", "The survey shows long customer relationships."));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
Assert.That(issue, Does.Contain("\"s1\" is used more than once"));
});
}
}
[Test]
@@ -73,11 +73,11 @@ public sealed class SwotAnalysisModerationTests
var moderation = CreateModeration();
moderation.PrioritizedActions.Items.Single().AddressedFactorIds = ["S1", "X9"];
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
Assert.That(issue, Does.Contain("\"X9\""));
});
}
}
[Test]
@@ -86,11 +86,11 @@ public sealed class SwotAnalysisModerationTests
var moderation = CreateModeration();
moderation.OverallAssessment.Text = " ";
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
Assert.That(issue, Is.EqualTo("The field overall_assessment.text is empty."));
});
}
}
[Test]
@@ -102,22 +102,22 @@ public sealed class SwotAnalysisModerationTests
withReason.ModerationNotes.Items.Clear();
withReason.ModerationNotes.EmptyMessage = "The four categories needed no changes.";
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(withoutReason), out _, out var issue), Is.False);
Assert.That(issue, Does.StartWith("moderation_notes has no items"));
Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(withReason), out _, out _), Is.True);
});
}
}
[Test]
public void AnAnswerWithoutJsonNamesTheProblemForTheModel()
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(SwotAnalysisModerationResponseParser.TryParse("Here is my assessment.", out _, out var issue), Is.False);
Assert.That(issue, Does.Contain("no complete JSON object"));
});
}
}
[Test]
@@ -125,7 +125,7 @@ public sealed class SwotAnalysisModerationTests
{
var request = SwotAnalysisModeration.BuildUserRequest(CreateCategories(), "Support the investment decision", string.Empty, "A market report documents increasing demand.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
foreach (var category in new[] { "strengths", "weaknesses", "opportunities", "threats" })
Assert.That(request, Does.Contain($"\"{category}\""));
@@ -137,7 +137,7 @@ public sealed class SwotAnalysisModerationTests
Assert.That(request, Does.Match("(?s)<analysis-focus>\\s*No additional analysis focus was provided\\.\\s*</analysis-focus>"));
Assert.That(request, Does.Match("(?s)<shared-research>\\s*A market report documents increasing demand\\.\\s*</shared-research>"));
Assert.That(request, Does.Not.Contain("<analysis-subject>"), "The moderator works on the results, not on the material.");
});
}
}
[Test]
@@ -149,13 +149,13 @@ public sealed class SwotAnalysisModerationTests
{
var prompt = SwotAnalysisModeration.BuildSystemPrompt("Answer in German.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
foreach (var key in new[] { "\"moderation_notes\"", "\"overall_assessment\"", "\"prioritized_actions\"", "\"addressed_factor_ids\"", "\"factor_id\"" })
Assert.That(prompt, Does.Contain(key));
Assert.That(prompt, Does.EndWith("Answer in German."));
});
}
}
private static Dictionary<SwotAnalysisCategory, SwotCategoryAnalysisResult> CreateCategories() => new()
@@ -18,7 +18,7 @@ public sealed class SwotAnalysisSendToChatTests
var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, visiblePrompt, result);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(chatThread.Blocks, Has.Count.EqualTo(2));
Assert.That(chatThread.Blocks[0], Is.SameAs(visiblePrompt));
@@ -27,7 +27,7 @@ public sealed class SwotAnalysisSendToChatTests
Assert.That(((ContentText)chatThread.Blocks[1].Content!).Text, Is.EqualTo("## SWOT Matrix"));
Assert.That(chatThread.Blocks[1].Content, Is.Not.SameAs(result.Content));
Assert.That(chatThread.SystemPrompt, Is.EqualTo(SystemPrompts.DEFAULT));
});
}
}
[Test]
@@ -38,11 +38,11 @@ public sealed class SwotAnalysisSendToChatTests
var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, VisiblePrompt(result.Time.AddTicks(-1)), result);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(assistantThread.IsWebAddressGivenToTheModel(new Uri(ADDRESS_A_MODEL_CHOSE)), Is.True, "The hidden request counts as a message of the user.");
Assert.That(chatThread.IsWebAddressGivenToTheModel(new Uri(ADDRESS_A_MODEL_CHOSE)), Is.False);
});
}
}
[Test]
@@ -54,11 +54,11 @@ public sealed class SwotAnalysisSendToChatTests
var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, VisiblePrompt(result.Time.AddTicks(-1)), result);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(chatThread.ChatId, Is.EqualTo(assistantThread.ChatId));
Assert.That(chatThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
});
}
}
[Test]
@@ -21,12 +21,12 @@ public sealed class SwotAnalysisSessionStateTests
var reader = new AssistantSessionStateReader(writer.ToDictionary(), "SWOT Analysis");
reader.Restore(key, value => restored = value);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(restored, Is.Not.Null);
Assert.That(restored!.MatrixHeading, Is.EqualTo("SWOT Matrix"));
Assert.That(restored.Strengths.Findings.Single().Summary, Is.EqualTo("Experienced team"));
});
}
}
[Test]
@@ -44,12 +44,12 @@ public sealed class SwotAnalysisSessionStateTests
var reader = new AssistantSessionStateReader(writer.ToDictionary(), "SWOT Analysis");
reader.RestoreDictionary(key, restored);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(restored[SwotAnalysisCategory.STRENGTHS].Status, Is.EqualTo(SwotCategoryAnalysisStatus.COMPLETED));
Assert.That(restored[SwotAnalysisCategory.STRENGTHS].Result!.Findings.Single().Summary, Is.EqualTo("Experienced team"));
Assert.That(restored[SwotAnalysisCategory.WEAKNESSES].Status, Is.EqualTo(SwotCategoryAnalysisStatus.RUNNING));
});
}
}
private static SwotFinding Finding() => new()
@@ -15,13 +15,13 @@ public sealed class SwotCategoryAnalysisResponseTests
var parsed = SwotCategoryAnalysisResponseParser.TryParse(JsonSerializer.Serialize(result), out var parsedResult, out var issue);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parsed, Is.True);
Assert.That(issue, Is.Empty);
Assert.That(parsedResult.EmptyMessage, Is.Empty);
Assert.That(parsedResult.Findings.Single().Summary, Is.EqualTo("Experienced team"));
});
}
}
[Test]
@@ -29,11 +29,11 @@ public sealed class SwotCategoryAnalysisResponseTests
{
var parsed = SwotCategoryAnalysisResponseParser.TryParse("I cannot analyze this category.", out _, out var issue);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parsed, Is.False);
Assert.That(issue, Does.Contain("no complete JSON object"));
});
}
}
[Test]
@@ -41,11 +41,11 @@ public sealed class SwotCategoryAnalysisResponseTests
{
var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"findings": [{"summary": "Experienced team",}""", out _, out var issue);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parsed, Is.False);
Assert.That(issue, Is.Not.Empty);
});
}
}
[Test]
@@ -53,11 +53,11 @@ public sealed class SwotCategoryAnalysisResponseTests
{
var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"empty_message": null, "findings": []}""", out _, out var issue);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parsed, Is.False);
Assert.That(issue, Does.StartWith("The JSON object cannot be read:"));
});
}
}
[Test]
@@ -65,11 +65,11 @@ public sealed class SwotCategoryAnalysisResponseTests
{
var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"empty_message": "", "findings": [null, {"summary": "Experienced team", "explanation": "Long tenure."}]}""", out var result, out _);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parsed, Is.True);
Assert.That(result.Findings.Single().Summary, Is.EqualTo("Experienced team"));
});
}
}
[Test]
@@ -81,13 +81,13 @@ public sealed class SwotCategoryAnalysisResponseTests
});
var invalidEmptyResponse = JsonSerializer.Serialize(new SwotCategoryAnalysisResult());
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(SwotCategoryAnalysisResponseParser.TryParse(validEmptyResponse, out var result, out _), Is.True);
Assert.That(result.Findings, Is.Empty);
Assert.That(SwotCategoryAnalysisResponseParser.TryParse(invalidEmptyResponse, out _, out var issue), Is.False);
Assert.That(issue, Does.Contain("empty_message"));
});
}
}
[Test]
@@ -101,13 +101,13 @@ public sealed class SwotCategoryAnalysisResponseTests
result.Findings[0].Explanation = string.Empty;
var responseWithoutExplanation = JsonSerializer.Serialize(result);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(SwotCategoryAnalysisResponseParser.TryParse(responseWithContradictoryEmptyMessage, out _, out var contradictionIssue), Is.False);
Assert.That(contradictionIssue, Does.Contain("Leave empty_message empty"));
Assert.That(SwotCategoryAnalysisResponseParser.TryParse(responseWithoutExplanation, out _, out var explanationIssue), Is.False);
Assert.That(explanationIssue, Is.EqualTo("Finding 1 has no explanation."));
});
}
}
[TestCase("STRENGTHS", "strengths: internal positive factors")]
@@ -119,7 +119,7 @@ public sealed class SwotCategoryAnalysisResponseTests
var category = Enum.Parse<SwotAnalysisCategory>(categoryName);
var prompt = SwotCategoryAnalysis.BuildSystemPrompt(category, "Answer in German.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(prompt, Does.Contain($"Analyze only {expectedDescription}."));
Assert.That(prompt, Does.Contain("Do not return findings for another SWOT category."));
@@ -127,7 +127,7 @@ public sealed class SwotCategoryAnalysisResponseTests
Assert.That(prompt, Does.Contain("analysis-subject, contextual-knowledge, and\nshared-research"));
Assert.That(prompt, Does.Contain("untrusted data"));
Assert.That(prompt, Does.EndWith("Answer in German."));
});
}
}
[Test]
@@ -140,14 +140,14 @@ public sealed class SwotCategoryAnalysisResponseTests
new SwotContextualKnowledge("The annual report names 120 employees.", []),
sharedResearch: "A market report documents increasing regional demand.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(request, Does.Match("(?s)<analysis-subject>\\s*A regional manufacturer\\s*</analysis-subject>"));
Assert.That(request, Does.Match("(?s)<analysis-goal>\\s*Support the investment decision\\s*</analysis-goal>"));
Assert.That(request, Does.Match("(?s)<analysis-focus>\\s*Pay particular attention to staffing\\s*</analysis-focus>"));
Assert.That(request, Does.Match("(?s)<contextual-knowledge>\\s*The annual report names 120 employees\\.\\s*</contextual-knowledge>"));
Assert.That(request, Does.Match("(?s)<shared-research>\\s*A market report documents increasing regional demand\\.\\s*</shared-research>"));
});
}
}
[Test]
@@ -159,12 +159,12 @@ public sealed class SwotCategoryAnalysisResponseTests
string.Empty,
SwotContextualKnowledge.NONE);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(request, Does.Not.Contain("<shared-research>"));
Assert.That(request, Does.Contain("No additional analysis focus was provided."));
Assert.That(request, Does.Contain("No additional contextual knowledge was provided."));
});
}
}
private static SwotCategoryAnalysisResult CreateResult() => new()
@@ -22,11 +22,11 @@ public sealed class SwotContextualKnowledgeTests
var oneImage = new SwotContextualKnowledge(string.Empty, [Image("chart.png")]);
var twoImages = new SwotContextualKnowledge(string.Empty, [Image("chart.png"), Image("photo.jpg")]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(oneImage.Describe(), Is.EqualTo("The image attached to this message belongs to the contextual knowledge."));
Assert.That(twoImages.Describe(), Is.EqualTo("The 2 images attached to this message belong to the contextual knowledge."));
});
}
}
[Test]
@@ -34,11 +34,11 @@ public sealed class SwotContextualKnowledgeTests
{
var description = new SwotContextualKnowledge(DOCUMENTS, [Image("chart.png")]).Describe();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(description, Does.StartWith(DOCUMENTS));
Assert.That(description, Does.EndWith("The image attached to this message belongs to the contextual knowledge."));
});
}
}
private static FileAttachmentImage Image(string fileName) => new(fileName, $"/tmp/{fileName}", 1024);
@@ -13,10 +13,10 @@ public sealed class SwotQuadrantLabelsTests
[TestCase(SwotAnalysisCategory.THREATS, "Risiken", "Extern · Negativ")]
public void EveryQuadrantNamesItselfAndWhereItLies(SwotAnalysisCategory category, string expectedName, string expectedAxes)
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(LABELS.NameOf(category), Is.EqualTo(expectedName));
Assert.That(LABELS.AxesOf(category), Is.EqualTo(expectedAxes));
});
}
}
}
@@ -14,11 +14,11 @@ public sealed class SwotRepairRoundTests
{
var request = SwotModelAnswer.RepairRequestOf("Finding 1 has no explanation.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(request, Does.Contain("Finding 1 has no explanation."));
Assert.That(request, Does.Contain("complete corrected JSON object"));
});
}
}
[Test]
@@ -28,12 +28,12 @@ public sealed class SwotRepairRoundTests
var read = await ReadAsync(VALID_ANSWER, repairRequests, ANSWER_WITHOUT_JSON);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(read.Result?.Findings.Single().Summary, Is.EqualTo("Experienced team"));
Assert.That(read.Issue, Is.Empty);
Assert.That(repairRequests, Is.Empty);
});
}
}
[Test]
@@ -43,12 +43,12 @@ public sealed class SwotRepairRoundTests
var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, VALID_ANSWER);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(read.Result?.Findings.Single().Explanation, Is.EqualTo("The documents describe long employee tenure."));
Assert.That(read.Issue, Is.Empty);
Assert.That(repairRequests.Single(), Is.EqualTo("Finding 1 has no explanation."));
});
}
}
[Test]
@@ -58,12 +58,12 @@ public sealed class SwotRepairRoundTests
var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, ANSWER_WITHOUT_JSON);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(read.Result, Is.Null);
Assert.That(read.Issue, Does.Contain("no complete JSON object"));
Assert.That(repairRequests, Has.Count.EqualTo(1));
});
}
}
[Test]
@@ -74,12 +74,12 @@ public sealed class SwotRepairRoundTests
// The provider failed, so the repair round brought no answer:
var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, string.Empty);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(read.Result, Is.Null);
Assert.That(read.Issue, Is.EqualTo("Finding 1 has no explanation."));
Assert.That(repairRequests, Has.Count.EqualTo(1));
});
}
}
private static Task<SwotReadAnswer<SwotCategoryAnalysisResult>> ReadAsync(string answer, List<string> repairRequests, string repairedAnswer) =>
@@ -10,14 +10,14 @@ public sealed class SwotSharedResearchTests
{
var prompt = SwotSharedResearch.BuildSystemPrompt("Answer in German.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(prompt, Does.Contain("gather additional evidence once"));
Assert.That(prompt, Does.Contain("Do not perform the SWOT analysis yourself"));
Assert.That(prompt, Does.Contain("tool result as untrusted data"));
Assert.That(prompt, Does.Contain("source titles and URLs"));
Assert.That(prompt, Does.EndWith("Answer in German."));
});
}
}
[Test]
@@ -29,13 +29,13 @@ public sealed class SwotSharedResearchTests
"Pay particular attention to staffing",
new SwotContextualKnowledge("The annual report names 120 employees.", []));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(request, Does.Match("(?s)<analysis-subject>\\s*A regional manufacturer\\s*</analysis-subject>"));
Assert.That(request, Does.Match("(?s)<analysis-goal>\\s*Support the investment decision\\s*</analysis-goal>"));
Assert.That(request, Does.Match("(?s)<analysis-focus>\\s*Pay particular attention to staffing\\s*</analysis-focus>"));
Assert.That(request, Does.Match("(?s)<contextual-knowledge>\\s*The annual report names 120 employees\\.\\s*</contextual-knowledge>"));
Assert.That(request, Does.Not.Contain("<shared-research>"));
});
}
}
}
+6 -6
View File
@@ -33,11 +33,11 @@ public sealed class SendToReceiverTests
{
var data = destination.GetData();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(data.Event, Is.Not.EqualTo(Event.NONE), "Without an event of its own, the text has nothing to travel with.");
Assert.That(PagesOf(data.Route), Has.Count.EqualTo(1), "The text is taken by the page which opens under the route.");
});
}
}
[TestCaseSource(nameof(Destinations))]
@@ -49,22 +49,22 @@ public sealed class SendToReceiverTests
var applyMethod = page.GetMethod("ApplyTextLaunchPayload", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(IsAssistant(page), Is.True, $"{page.Name} is expected to be an assistant.");
Assert.That(applyMethod, Is.Not.Null);
Assert.That(applyMethod!.DeclaringType, Is.Not.EqualTo(applyMethod.GetBaseDefinition().DeclaringType), $"{page.Name} allows sending to it, so it has to put the text sent to it into a field.");
});
}
}
[Test]
public void TextForTheChatGoesIntoTheFieldForTheNextMessage()
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(AssistantLaunchService.TextEventFor(AIStudio.Tools.Components.CHAT), Is.EqualTo(Event.SEND_TO_CHAT_INPUT));
Assert.That(AssistantLaunchService.TextEventFor(AIStudio.Tools.Components.SLIDE_BUILDER_ASSISTANT), Is.EqualTo(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT));
});
}
}
private static List<Type> PagesOf(string route) => APP.GetTypes()
@@ -67,11 +67,11 @@ public sealed class VisualBriefingRestrictionsTests
var loaded = await this.CreateStore().LoadAsync(created.BriefingId);
Assert.That(loaded, Is.Not.Null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(loaded!.Settings.Instruction, Is.EqualTo("Summarize the minutes."), "The save did not happen.");
AssertSame(loaded.Restrictions, FROM_MAILBOX);
});
}
}
[Test]
@@ -127,23 +127,26 @@ public sealed class VisualBriefingRestrictionsTests
var exception = Assert.Throws<VisualBriefingBuildException>(() => VisualBriefingBuildOrchestrator.ValidateProviderMaySeeContent(restricted, cloudProvider));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(exception!.Code, Is.EqualTo(VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED));
Assert.DoesNotThrow(() => VisualBriefingBuildOrchestrator.ValidateProviderMaySeeContent(restricted, selfHostedProvider));
Assert.DoesNotThrow(() => VisualBriefingBuildOrchestrator.ValidateProviderMaySeeContent(new VisualBriefingManifest(), cloudProvider), "A briefing nothing was sent to may use any provider.");
});
}
}
private VisualBriefingStore CreateStore() => new(null!, NullLogger<VisualBriefingStore>.Instance, new VisualBriefingStorageOptions { DataDirectory = this.dataDirectory });
private static void AssertSame(ChatRestrictions actual, ChatRestrictions expected) => Assert.Multiple(() =>
private static void AssertSame(ChatRestrictions actual, ChatRestrictions expected)
{
Assert.That(actual.ProviderConfidence, Is.EqualTo(expected.ProviderConfidence));
Assert.That(actual.DataSecurity, Is.EqualTo(expected.DataSecurity));
Assert.That(actual.OutboundData, Is.EqualTo(expected.OutboundData));
Assert.That(actual.WebAddressesWrittenByModels, Is.EquivalentTo(expected.WebAddressesWrittenByModels));
});
using (Assert.EnterMultipleScope())
{
Assert.That(actual.ProviderConfidence, Is.EqualTo(expected.ProviderConfidence));
Assert.That(actual.DataSecurity, Is.EqualTo(expected.DataSecurity));
Assert.That(actual.OutboundData, Is.EqualTo(expected.OutboundData));
Assert.That(actual.WebAddressesWrittenByModels, Is.EquivalentTo(expected.WebAddressesWrittenByModels));
}
}
private AIStudio.Settings.Provider AddProvider(string id, LLMProviders llmProvider, bool isSelfHosted)
{
+2 -2
View File
@@ -36,12 +36,12 @@ public sealed class AttachingAFileAgainTests
File.WriteAllText(path, "The board met on Monday and on Tuesday.");
attachments.AttachInPlaceOfSameFile(FileAttachment.FromPath(path));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(attachments.Count(attachment => attachment.FilePath == path), Is.EqualTo(1), "The message would go with the old and the new text of the file.");
Assert.That(attachments.Single(attachment => attachment.FilePath == path).ProvenanceId, Is.Null, "Attaching a file again means the file as it is now.");
Assert.That(attachments, Does.Contain(otherFile));
});
}
}
[Test]
+24 -24
View File
@@ -81,13 +81,13 @@ public sealed class AttachmentProvenanceTests
var firstRequest = await this.PrepareAsync(message);
var secondRequest = await this.PrepareAsync(message);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(message.FileAttachments.Single().ProvenanceId, Is.Not.Null);
Assert.That(firstRequest, Does.Contain("The board met on Monday."));
Assert.That(secondRequest, Is.EqualTo(firstRequest));
Assert.That(this.runtime.ReadFiles, Has.Count.EqualTo(1), "Every request read every document anew, which a long chat with large documents paid for with every message.");
});
}
}
[Test]
@@ -100,11 +100,11 @@ public sealed class AttachmentProvenanceTests
await message.StoreAttachedDocumentsAsync(this.store.Client, Guid.NewGuid(), this.rustService, null, readChangedAgain: false);
var prepared = await this.PrepareAsync(message);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(prepared, Does.Contain("The board met on Monday."), "Every request of the chat sends the same text, which is what the user can look at.");
Assert.That(prepared, Does.Not.Contain("never met"));
});
}
}
[Test]
@@ -119,13 +119,13 @@ public sealed class AttachmentProvenanceTests
await message.StoreAttachedDocumentsAsync(this.store.Client, chatId, this.rustService, null, readChangedAgain: true);
var prepared = await this.PrepareAsync(message);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(prepared, Does.Contain("and on Tuesday"), "The user chose that the AI works with the files as they are now.");
Assert.That(message.FileAttachments.Single().ProvenanceId, Is.Not.EqualTo(firstId), "The text sent before belongs to what the AI received then.");
Assert.That(message.FileAttachments.Single().Version, Is.EqualTo(FileVersion.Of(this.documentPath)));
Assert.That(this.runtime.ReadFiles, Has.Count.EqualTo(2));
});
}
}
[Test]
@@ -138,12 +138,12 @@ public sealed class AttachmentProvenanceTests
await message.StoreAttachedDocumentsAsync(this.store.Client, chatId, this.rustService, null, readChangedAgain: true);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(message.HasDocumentsToStore(readChangedAgain: true), Is.False);
Assert.That(message.FileAttachments.Single().ProvenanceId, Is.EqualTo(firstId));
Assert.That(this.runtime.ReadFiles, Has.Count.EqualTo(1));
});
}
}
[Test]
@@ -155,11 +155,11 @@ public sealed class AttachmentProvenanceTests
File.Delete(this.documentPath);
var prepared = await this.PrepareAsync(message);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(message.HasDocumentsToStore(readChangedAgain: true), Is.False, "A file which is gone did not change: its stored text is all there is.");
Assert.That(prepared, Does.Contain("The board met on Monday."));
});
}
}
[Test]
@@ -184,12 +184,12 @@ public sealed class AttachmentProvenanceTests
var provenanceId = message.FileAttachments.Single().ProvenanceId!.Value;
var entries = await this.store.Client.GetEntriesAsync([provenanceId], CancellationToken.None);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(entries[provenanceId].Content, Does.Contain(FakeRuntime.REDACTION_MARKER), "The store keeps what the AI received, not what the file holds.");
Assert.That(entries[provenanceId].Content, Does.Not.Contain(SUSPICIOUS_PASSAGE));
Assert.That(entries[provenanceId].Kind, Is.EqualTo(ProvenanceKind.ATTACHMENT));
});
}
}
[Test]
@@ -202,13 +202,13 @@ public sealed class AttachmentProvenanceTests
var prepared = await this.PrepareAsync(message);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(prepared, Does.Contain("The board met on Monday."));
Assert.That(prepared, Does.Not.Contain(SUSPICIOUS_PASSAGE), "Every way of content to the AI goes through the filter, with the rules of today.");
Assert.That(prepared, Does.Contain(FakeRuntime.REDACTION_MARKER));
Assert.That(this.runtime.ReadFiles, Is.Empty, "The stored text is filtered, not the file read again.");
});
}
}
[Test]
@@ -222,13 +222,13 @@ public sealed class AttachmentProvenanceTests
await message.StoreAttachedDocumentsAsync(this.store.Client, chatId, this.rustService, null, readChangedAgain: false);
await using var context = this.store.CreateContext();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(message.HasDocumentsToStore(readChangedAgain: false), Is.False);
Assert.That(message.FileAttachments.Single().ProvenanceId, Is.EqualTo(firstId));
Assert.That(context.Entries.Count(), Is.EqualTo(1));
Assert.That(this.runtime.ReadFiles, Has.Count.EqualTo(1));
});
}
}
[Test]
@@ -239,11 +239,11 @@ public sealed class AttachmentProvenanceTests
await message.StoreAttachedDocumentsAsync(new NoProvenanceStoreClient("SQLite", "not available"), Guid.NewGuid(), this.rustService, null, readChangedAgain: false);
var prepared = await this.PrepareAsync(message);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(message.FileAttachments.Single().ProvenanceId, Is.Null);
Assert.That(prepared, Does.Contain("The board met on Monday."), "Without the store, a chat works as it always did.");
});
}
}
[Test]
@@ -253,11 +253,11 @@ public sealed class AttachmentProvenanceTests
var prepared = await this.PrepareAsync(message);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(prepared, Does.Contain("The board met on Monday."), "A store which lost the entry, e.g., one restored from an older backup, must not cost the document.");
Assert.That(this.runtime.ReadFiles, Has.Count.EqualTo(1));
});
}
}
[Test]
@@ -270,12 +270,12 @@ public sealed class AttachmentProvenanceTests
await message.StoreAttachedDocumentsAsync(this.store.Client, Guid.NewGuid(), this.rustService, null, readChangedAgain: false);
var chatsWithEntries = await this.store.Client.GetChatIdsAsync(CancellationToken.None);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(message.HasDocumentsToStore(readChangedAgain: false), Is.False, "An image goes to the AI as an image, not as a text to keep.");
Assert.That(message.FileAttachments.Single().ProvenanceId, Is.Null);
Assert.That(chatsWithEntries, Is.Empty);
});
}
}
[Test]
@@ -289,7 +289,7 @@ public sealed class AttachmentProvenanceTests
using var document = JsonDocument.Parse(json);
var storedAttachments = document.RootElement.GetProperty("file_attachments");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(attachment.Normalize().ProvenanceId, Is.EqualTo(attachment.ProvenanceId), "Every request normalizes its attachments first.");
Assert.That(attachment.Normalize().Version, Is.EqualTo(attachment.Version), "Otherwise, a chat which reads changed files again would read every file again.");
@@ -300,7 +300,7 @@ public sealed class AttachmentProvenanceTests
Assert.That(transcript.WithFilePath(Path.Combine(this.directoryPath, "moved.md")).ProvenanceId, Is.EqualTo(transcript.ProvenanceId));
Assert.That(loaded.FileAttachments[0].ProvenanceId, Is.EqualTo(attachment.ProvenanceId));
Assert.That(storedAttachments[1].TryGetProperty("provenance_id", out _), Is.False, "An attachment never sent is stored as before, also in a chat template.");
});
}
}
private ContentText MessageWithDocument() => new() { Text = "Summarize the minutes.", FileAttachments = [FileAttachment.FromPath(this.documentPath)] };
+26 -26
View File
@@ -25,11 +25,11 @@ public sealed class ChatBlockActionsTests
var removed = WhileWriting(chat).TryRemove(chat, answer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(removed, Is.False);
Assert.That(chat.Blocks.Select(x => x.Content), Is.EqualTo(new[] { question.Content, answer.Content }), "The answer stays, since the job goes on writing into it.");
});
}
}
[Test]
@@ -41,13 +41,13 @@ public sealed class ChatBlockActionsTests
var blocks = chat.Blocks.ToList();
var actions = WhileWriting(chat);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(actions.TryRemove(chat, firstQuestion.Content!), Is.False);
Assert.That(actions.TryRemove(chat, firstAnswer.Content!), Is.False);
Assert.That(actions.TryRemoveForRegenerate(chat, firstAnswer.Content!), Is.False);
Assert.That(chat.Blocks, Is.EqualTo(blocks), "The job stores the conversation meanwhile, so no message may change, not even an older one.");
});
}
}
[Test]
@@ -58,13 +58,13 @@ public sealed class ChatBlockActionsTests
var chat = Chat(question, answer);
var actions = WhileWriting(chat);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(actions.TryEditLastUserBlock(chat, question.Content!), Is.False);
Assert.That(actions.TryEditLastBlock(chat, question.Content!), Is.False);
Assert.That(chat.Blocks, Has.Count.EqualTo(2), "Both the question and the answer being written stay.");
Assert.That(chat.Draft.IsEmpty, Is.True, "The question does not become the draft, since it could not be sent anyway.");
});
}
}
[Test]
@@ -75,11 +75,11 @@ public sealed class ChatBlockActionsTests
var removed = WhileWriting(chat).TryRemoveForRegenerate(chat, answer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(removed, Is.False, "Regenerating would leave a second answer next to the one being written.");
Assert.That(chat.Blocks, Has.Count.EqualTo(2));
});
}
}
[Test]
@@ -90,11 +90,11 @@ public sealed class ChatBlockActionsTests
var rolledBack = WhileWriting(chat).TryRollBack(chat, firstAnswer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(rolledBack, Is.False);
Assert.That(chat.Blocks, Has.Count.EqualTo(4));
});
}
}
[Test]
@@ -106,11 +106,11 @@ public sealed class ChatBlockActionsTests
var removed = Done().TryRemove(chat, answer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(removed, Is.True);
Assert.That(chat.Blocks.Select(x => x.Content), Is.EqualTo(new[] { question.Content }));
});
}
}
[Test]
@@ -122,11 +122,11 @@ public sealed class ChatBlockActionsTests
var removed = Done().TryRemoveForRegenerate(chat, answer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(removed, Is.True);
Assert.That(chat.Blocks.Select(x => x.Content), Is.EqualTo(new[] { question.Content }), "The question stays, to be answered anew.");
});
}
}
[Test]
@@ -138,11 +138,11 @@ public sealed class ChatBlockActionsTests
var rolledBack = Done().TryRollBack(chat, firstAnswer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(rolledBack, Is.True);
Assert.That(chat.Blocks.Select(x => x.Content), Is.EqualTo(new[] { firstQuestion.Content, firstAnswer.Content }));
});
}
}
[Test]
@@ -153,12 +153,12 @@ public sealed class ChatBlockActionsTests
var edited = Done().TryEditLastUserBlock(chat, question.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(edited, Is.True);
Assert.That(chat.Blocks, Is.Empty, "The question goes back into the composer, and the answer to it goes.");
Assert.That(chat.Draft.Text, Is.EqualTo("Question"));
});
}
}
[Test]
@@ -169,12 +169,12 @@ public sealed class ChatBlockActionsTests
var edited = Done().TryEditLastBlock(chat, question.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(edited, Is.True);
Assert.That(chat.Blocks, Is.Empty);
Assert.That(chat.Draft.Text, Is.EqualTo("Question"));
});
}
}
[Test]
@@ -185,12 +185,12 @@ public sealed class ChatBlockActionsTests
var edited = Done().TryEditLastUserBlock(chat, new ContentText { Text = "Question of another chat" });
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(edited, Is.False);
Assert.That(chat.Blocks, Has.Count.EqualTo(2), "Removing the last answer only makes sense together with the question to it.");
Assert.That(chat.Draft.IsEmpty, Is.True);
});
}
}
[Test]
@@ -208,11 +208,11 @@ public sealed class ChatBlockActionsTests
var removed = actions.TryRemove(chat, answer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(removed, Is.True);
Assert.That(askedChatIds, Is.EqualTo(new[] { chat.ChatId }), "Only the chat being changed decides.");
});
}
}
[Test]
@@ -221,7 +221,7 @@ public sealed class ChatBlockActionsTests
var actions = Done();
var content = new ContentText { Text = "Question" };
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(actions.AreAllowed(null), Is.False);
Assert.That(actions.TryRemove(null, content), Is.False);
@@ -229,7 +229,7 @@ public sealed class ChatBlockActionsTests
Assert.That(actions.TryRollBack(null, content), Is.False);
Assert.That(actions.TryEditLastUserBlock(null, content), Is.False);
Assert.That(actions.TryEditLastBlock(null, content), Is.False);
});
}
}
/// <summary>
+2 -2
View File
@@ -62,11 +62,11 @@ public sealed class ChatDraftAutosaveTests
storage.IsBusy = false;
await storage.WaitForStoreAsync();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(storedWhileBusy, Is.Zero, "The AI job changes the conversation meanwhile, so storing it from elsewhere could read it in the middle of a change.");
Assert.That(storage.StoredChats, Is.EqualTo(new[] { chat }), "Once the answer is done, the change must still reach the disk.");
});
}
}
[Test]
+16 -16
View File
@@ -46,12 +46,12 @@ public sealed class ChatDraftTests
var added = draft.AddAttachments([TRANSCRIPT]);
var addedAgain = draft.AddAttachments([TRANSCRIPT with { FileSizeBytes = 1024 }]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(added, Is.True);
Assert.That(addedAgain, Is.False, "The same file counts once, even in a newer state.");
Assert.That(draft.FileAttachments, Is.EqualTo(new FileAttachment[] { TRANSCRIPT }), "The composer would show the transcript twice otherwise.");
});
}
}
[Test]
@@ -74,11 +74,11 @@ public sealed class ChatDraftTests
draft.ReplaceAttachments(attachment => attachment with { FilePath = $"/moved{attachment.FilePath}" });
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(draft.FileAttachments.Select(attachment => attachment.FilePath), Is.EqualTo(new[] { "/moved/documents/agenda.pdf", "/moved/data/tempChats/meeting-transcript-0001.md" }));
Assert.That(draft.MediaTranscripts, Has.Count.EqualTo(1), "A transcript stays a transcript, so removing it from the composer still deletes its file.");
});
}
}
[Test]
@@ -90,11 +90,11 @@ public sealed class ChatDraftTests
draft.RestoreFromMessage(new ContentText { Text = "Summarize the minutes", FileAttachments = [MINUTES] });
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(draft.Text, Is.EqualTo("Summarize the minutes"), "The message to edit replaces the older draft.");
Assert.That(draft.FileAttachments, Is.EqualTo(new[] { MINUTES, TRANSCRIPT }), "The draft is the only owner of its transcripts, so dropping them would leave their files behind.");
});
}
}
[Test]
@@ -107,11 +107,11 @@ public sealed class ChatDraftTests
draft.Clear();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(draft.IsEmpty, Is.True, "The attachments belong to the sent message now, and a transcript has only one owner.");
Assert.That(draft.PreviewedTexts, Is.Empty, "A text the preview read went with the message, or is of no use to the next one.");
});
}
}
[Test]
@@ -126,13 +126,13 @@ public sealed class ChatDraftTests
draft.NotePreviewedText(otherFile);
var replacedBySecond = draft.NotePreviewedText(secondPreview);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(replacedByFirst, Is.Null);
Assert.That(replacedBySecond, Is.EqualTo(firstPreview.ProvenanceId), "Nothing refers to the text of the first preview any longer, so the caller removes it from the store.");
Assert.That(draft.PreviewedTexts, Is.EquivalentTo(new[] { otherFile, secondPreview }));
Assert.That(draft.GetPreviewedText(AGENDA.FilePath), Is.EqualTo(secondPreview));
});
}
}
[Test]
@@ -148,12 +148,12 @@ public sealed class ChatDraftTests
draft.MirrorAttachments([MINUTES]);
var forgotten = draft.ForgetPreviewsOfDetachedFiles();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(forgotten, Is.EqualTo(new[] { agendaPreview.ProvenanceId }), "Nothing refers to the text any longer, so the caller removes it from the store.");
Assert.That(draft.PreviewedTexts, Is.EqualTo(new[] { minutesPreview }));
Assert.That(draft.ForgetPreviewsOfDetachedFiles(), Is.Empty);
});
}
}
[Test]
@@ -178,12 +178,12 @@ public sealed class ChatDraftTests
var loaded = JsonSerializer.Deserialize<ChatThread>(JsonSerializer.Serialize(thread, WorkspaceBehaviour.JSON_OPTIONS), WorkspaceBehaviour.JSON_OPTIONS)!;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(loaded.Draft.Text, Is.EqualTo("Summarize the meeting"));
Assert.That(loaded.Draft.FileAttachments, Is.EqualTo(new[] { AGENDA, CHART, STAGED_TRANSCRIPT }), "A transcript loaded as a plain document would no longer be managed, and its file would never be deleted.");
Assert.That(loaded.Draft.MediaTranscripts.Single().IsStaged, Is.True, "A staged transcript has to move into the chat directory once the chat is stored.");
});
}
}
[Test]
@@ -216,12 +216,12 @@ public sealed class ChatDraftTests
var loaded = JsonSerializer.Deserialize<ChatThread>(CHAT_WITH_PENDING_TRANSCRIPTS, WorkspaceBehaviour.JSON_OPTIONS)!;
var stored = JsonSerializer.Serialize(loaded, WorkspaceBehaviour.JSON_OPTIONS);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(loaded.Draft.FileAttachments, Is.EqualTo(new FileAttachment[] { TRANSCRIPT }), "Opening the chat has to bring the transcript back into the composer.");
Assert.That(stored, Does.Not.Contain("pending_media_transcripts"), "The draft holds the transcript now, so storing the chat must not write the former list again.");
Assert.That(stored, Does.Contain("\"draft\""));
});
}
}
[Test]
+25 -22
View File
@@ -56,14 +56,14 @@ public sealed class ChatRestrictionsTests
foreach (var combined in new[] { fromDataSource.StricterOf(fromMailbox), fromMailbox.StricterOf(fromDataSource) })
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(combined.ProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
Assert.That(combined.DataSecurity, Is.EqualTo(DataSourceSecurity.SELF_HOSTED), "Data for self-hosted providers only keeps them as the only choice, whichever side brings it.");
Assert.That(combined.OutboundData.Restriction, Is.EqualTo(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES));
Assert.That(combined.OutboundData.Reference, Is.EqualTo(MAILBOX_ID), "The source which demands the strictest restriction is the one to name.");
Assert.That(combined.WebAddressesWrittenByModels, Is.EquivalentTo(new[] { RequestKey("https://example.org/report"), RequestKey("https://example.org/minutes") }), "What models wrote on either side never counts as given.");
});
}
}
}
@@ -72,11 +72,11 @@ public sealed class ChatRestrictionsTests
{
var restrictions = ChatRestrictions.NONE.WithWebAddressesWrittenByModelIn("The report is on https://EXAMPLE.org/report#summary, and the minutes are on http://10.20.30.40:8000/minutes/. Write to ftp://example.org/upload.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(restrictions.WebAddressesWrittenByModels, Is.EquivalentTo(new[] { RequestKey("https://example.org/report"), RequestKey("http://10.20.30.40:8000/minutes/") }), "Only addresses a web page could be read from count, compared as the server gets asked for them.");
Assert.That(ChatRestrictions.NONE.WebAddressesWrittenByModels, Is.Empty, "Adding addresses leaves NONE as it is.");
});
}
}
[Test]
@@ -84,11 +84,11 @@ public sealed class ChatRestrictionsTests
{
var restrictions = new ChatRestrictions(ConfidenceLevel.MEDIUM, DataSourceSecurity.ALLOW_ANY, new(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataOrigin.DATA_SOURCE, DATA_SOURCE_ID), []).WithWebAddressesWrittenByModelIn("See https://example.org/report.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
AssertSame(restrictions.StricterOf(ChatRestrictions.NONE), restrictions);
AssertSame(ChatRestrictions.NONE.StricterOf(restrictions), restrictions);
});
}
}
[Test]
@@ -131,11 +131,11 @@ public sealed class ChatRestrictionsTests
chatToStart.RequireRestrictions(new(ConfidenceLevel.HIGH, DataSourceSecurity.SELF_HOSTED, new(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES, OutboundDataOrigin.MAILBOX, MAILBOX_ID), []));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
AssertSame(chatToStart.CaptureRestrictions(), new(ConfidenceLevel.HIGH, DataSourceSecurity.SELF_HOSTED, new(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES, OutboundDataOrigin.MAILBOX, MAILBOX_ID), []));
AssertSame(assistantThread.CaptureRestrictions(), ChatRestrictions.NONE);
});
}
}
[Test]
@@ -148,11 +148,11 @@ public sealed class ChatRestrictionsTests
chat.RequireProviderConfidence(ConfidenceLevel.HIGH);
chat.RequireDataSecurity(DataSourceSecurity.SELF_HOSTED);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(snapshot.ProviderConfidence, Is.EqualTo(ConfidenceLevel.LOW), "Content which left the chat does not hold what the chat read afterward.");
Assert.That(snapshot.DataSecurity, Is.EqualTo(DataSourceSecurity.NOT_SPECIFIED));
});
}
}
[Test]
@@ -162,12 +162,12 @@ public sealed class ChatRestrictionsTests
var selfHostedProvider = this.AddProvider(SELF_HOSTED_PROVIDER_ID, LLMProviders.SELF_HOSTED, isSelfHosted: true);
var restrictions = ChatRestrictions.NONE with { DataSecurity = DataSourceSecurity.SELF_HOSTED };
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(restrictions.IsLLMProviderAllowed(cloudProvider), Is.False);
Assert.That(restrictions.IsLLMProviderAllowed(selfHostedProvider), Is.True);
Assert.That(ChatRestrictions.NONE.IsLLMProviderAllowed(cloudProvider), Is.True, "Content which demands nothing may go to any provider.");
});
}
}
[Test]
@@ -178,11 +178,11 @@ public sealed class ChatRestrictionsTests
var higherLevels = Enum.GetValues<ConfidenceLevel>().Where(candidate => candidate > level).ToList();
Assume.That(higherLevels, Is.Not.Empty, "The provider has the highest level there is, so no content can demand more of it.");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That((ChatRestrictions.NONE with { ProviderConfidence = level }).IsLLMProviderAllowed(provider), Is.True);
Assert.That((ChatRestrictions.NONE with { ProviderConfidence = higherLevels.Min() }).IsLLMProviderAllowed(provider), Is.False);
});
}
}
[Test]
@@ -194,21 +194,24 @@ public sealed class ChatRestrictionsTests
chat.RequireDataSecurity(DataSourceSecurity.SELF_HOSTED);
ChatThread? newChat = null;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(chat.IsLLMProviderAllowed(cloudProvider), Is.EqualTo(chat.CaptureRestrictions().IsLLMProviderAllowed(cloudProvider)));
Assert.That(chat.IsLLMProviderAllowed(selfHostedProvider), Is.EqualTo(chat.CaptureRestrictions().IsLLMProviderAllowed(selfHostedProvider)));
Assert.That(newChat.IsLLMProviderAllowed(cloudProvider), Is.True, "A new chat demands nothing yet.");
});
}
}
private static void AssertSame(ChatRestrictions actual, ChatRestrictions expected) => Assert.Multiple(() =>
private static void AssertSame(ChatRestrictions actual, ChatRestrictions expected)
{
Assert.That(actual.ProviderConfidence, Is.EqualTo(expected.ProviderConfidence));
Assert.That(actual.DataSecurity, Is.EqualTo(expected.DataSecurity));
Assert.That(actual.OutboundData, Is.EqualTo(expected.OutboundData));
Assert.That(actual.WebAddressesWrittenByModels, Is.EquivalentTo(expected.WebAddressesWrittenByModels));
});
using (Assert.EnterMultipleScope())
{
Assert.That(actual.ProviderConfidence, Is.EqualTo(expected.ProviderConfidence));
Assert.That(actual.DataSecurity, Is.EqualTo(expected.DataSecurity));
Assert.That(actual.OutboundData, Is.EqualTo(expected.OutboundData));
Assert.That(actual.WebAddressesWrittenByModels, Is.EquivalentTo(expected.WebAddressesWrittenByModels));
}
}
private static string RequestKey(string address) => WebAddresses.CreateRequestKey(new Uri(address));
@@ -41,11 +41,11 @@ public sealed class ChatThreadAugmentedDataTests
var loaded = JsonSerializer.Deserialize<ChatThread>(CHAT_WITH_AUGMENTED_DATA, WorkspaceBehaviour.JSON_OPTIONS);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(loaded, Is.Not.Null, "An older chat file must still load.");
Assert.That(loaded!.AugmentedData, Is.Empty, "It belonged to a request long over; the next message retrieves anew.");
Assert.That(loaded.Name, Is.EqualTo("An older chat"));
});
}
}
}
+6 -6
View File
@@ -67,14 +67,14 @@ public sealed class ChatThreadEditTests
thread.Remove(answer.Content!);
composer.RestoreFromDraft(thread.Draft);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.Blocks, Is.Empty, "Both the question and the stopped answer leave the chat.");
Assert.That(File.Exists(transcript.FilePath), Is.True, "The composer still carries the transcript, so its file has to stay.");
Assert.That(composer.FileAttachments.Single().Exists, Is.True, "The edited message goes out with the transcript, not with a reference to a deleted file.");
Assert.That(thread.Draft.MediaTranscripts.Select(x => x.FilePath), Is.EqualTo(new[] { transcript.FilePath }), "The draft owns the transcript now: opening the chat again brings it back, and removing it from the composer deletes the file.");
Assert.That(thread.Draft.Text, Is.EqualTo("Summarize the meeting"), "The message being edited is the draft now, its text included.");
});
}
}
[Test]
@@ -101,11 +101,11 @@ public sealed class ChatThreadEditTests
thread.Remove(question.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(File.Exists(transcript.FilePath), Is.False, "Nothing refers to the transcript of a deleted message any longer, so its file has to go.");
Assert.That(thread.Draft.IsEmpty, Is.True, "A deleted message hands nothing over to the draft.");
});
}
}
[Test]
@@ -121,12 +121,12 @@ public sealed class ChatThreadEditTests
thread.RemoveForEditing(new ContentText { Text = "Question of another chat", FileAttachments = [transcript] });
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.Blocks, Has.Count.EqualTo(2), "The message to edit is not part of this chat, so no block goes.");
Assert.That(thread.Draft.IsEmpty, Is.True, "A message this chat does not hold never becomes its draft.");
Assert.That(File.Exists(transcript.FilePath), Is.True);
});
}
}
/// <summary>
+6 -6
View File
@@ -38,12 +38,12 @@ public sealed class ChatThreadFilePathTests
chat.RewriteFilePaths(Move);
var message = (ContentText)chat.Blocks[0].Content!;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(message.FileAttachments.Select(attachment => attachment.FilePath), Is.EqualTo(new[] { New("report.pdf"), New("meeting.md"), New("chart.png") }));
Assert.That(photo.Source, Is.EqualTo(New("photo.jpg")), "The model gets such an image from its path.");
Assert.That(chat.Draft.FileAttachments.Select(attachment => attachment.FilePath), Is.EqualTo(new[] { New("call.md") }), "The draft is the one place every path rewrite once forgot.");
});
}
}
[Test]
@@ -60,11 +60,11 @@ public sealed class ChatThreadFilePathTests
chat.RewriteFilePaths(Move);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(((ContentText)chat.Blocks[0].Content!).FileAttachments.Single(), Is.SameAs(elsewhere));
Assert.That(linked.Source, Is.EqualTo("https://example.org/chart.png"), "Only an image held by a local path has a path to rewrite.");
});
}
}
[Test]
@@ -78,14 +78,14 @@ public sealed class ChatThreadFilePathTests
var movedPicture = picture.WithFilePath(New("chart.png"));
var movedDocument = document.WithFilePath(New("report.pdf"));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(movedTranscript, Is.EqualTo(new ManagedTranscriptAttachment("meeting.md", New("meeting.md"), 512, "meeting.mp3", true)), "A transcript has to stay a transcript, or deleting the chat leaves its file behind.");
Assert.That(movedPicture, Is.TypeOf<FileAttachmentImage>());
Assert.That(((FileAttachmentImage)movedPicture).Source, Is.EqualTo(New("chart.png")), "The model gets the image from its source, not from its path.");
Assert.That(movedPicture.FilePathAsUrl, Is.EqualTo(new FileAttachmentImage("chart.png", New("chart.png"), 2048).FilePathAsUrl), "The preview of the image loads it from there.");
Assert.That(movedDocument, Is.EqualTo(new FileAttachment(FileAttachmentType.DOCUMENT, "report.pdf", New("report.pdf"), 1024)));
});
}
}
private static string Old(string fileName) => Path.Combine(OLD_DIRECTORY, fileName);
@@ -100,12 +100,12 @@ public sealed class ChatThreadOutboundDataRestrictionTests
[TestCase(OutboundDataRestriction.UNRESTRICTED)]
public void ARestrictionThisVersionDoesNotKnowCountsAsTheStrictest(OutboundDataRestriction known)
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(UNKNOWN_RESTRICTION.StricterOf(known), Is.EqualTo(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES));
Assert.That(known.StricterOf(UNKNOWN_RESTRICTION), Is.EqualTo(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES));
Assert.That(UNKNOWN_RESTRICTION.StricterOf(UNKNOWN_RESTRICTION), Is.EqualTo(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES));
});
}
}
[Test]
@@ -28,7 +28,7 @@ public sealed class ChatThreadReportedHistoryTests
var history = thread.ReportedHistoryFor(MODEL);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(history.IsKnown, Is.True);
Assert.That(history.PromptTokens, Is.EqualTo(1200));
@@ -38,7 +38,7 @@ public sealed class ChatThreadReportedHistoryTests
// would include the reasoning the next request never carries:
//
Assert.That(history.LastAnswer, Is.EqualTo("Answer 2"));
});
}
}
[Test]
@@ -54,12 +54,12 @@ public sealed class ChatThreadReportedHistoryTests
var thread = Thread(Question(1), Block(ChatRole.AI, reasoningOnly, 2));
var history = thread.ReportedHistoryFor(MODEL);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(history.IsKnown, Is.True);
Assert.That(history.PromptTokens, Is.EqualTo(1200));
Assert.That(history.LastAnswer, Is.Empty);
});
}
}
[Test]
@@ -135,12 +135,12 @@ public sealed class ChatThreadReportedHistoryTests
thread.Remove(lastAnswer.Content!);
var history = thread.ReportedHistoryFor(MODEL);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(history.IsKnown, Is.True, "The first answer is the last block again, with exactly the blocks it was reported for.");
Assert.That(history.PromptTokens, Is.EqualTo(1200));
Assert.That(history.LastAnswer, Is.EqualTo("Answer 2"));
});
}
}
[Test]
@@ -152,12 +152,12 @@ public sealed class ChatThreadReportedHistoryTests
thread.RollBackTo(firstAnswer.Content!);
var history = thread.ReportedHistoryFor(MODEL);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(history.IsKnown, Is.True);
Assert.That(history.PromptTokens, Is.EqualTo(1200));
Assert.That(history.LastAnswer, Is.EqualTo("Answer 2"));
});
}
}
private static ChatThread Thread(params ContentBlock[] blocks) => new()
@@ -24,13 +24,13 @@ public sealed class ChatThreadRestrictionsOfTests
assistantThread.RequireRestrictionsOf(researchThread);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.SELF_HOSTED));
Assert.That(assistantThread.RequiredOutboundDataRestriction.Restriction, Is.EqualTo(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT));
Assert.That(assistantThread.RequiredOutboundDataRestriction.Reference, Is.EqualTo(DATA_SOURCE_ID));
});
}
}
[Test]
@@ -60,12 +60,12 @@ public sealed class ChatThreadRestrictionsOfTests
assistantThread.RequireRestrictionsOf(researchThread);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.SELF_HOSTED));
Assert.That(assistantThread.RequiredOutboundDataRestriction.Restriction, Is.EqualTo(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES));
});
}
}
[Test]
@@ -75,12 +75,12 @@ public sealed class ChatThreadRestrictionsOfTests
assistantThread.RequireRestrictionsOf(new ChatThread());
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.NONE));
Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.NOT_SPECIFIED));
Assert.That(assistantThread.RequiredOutboundDataRestriction, Is.EqualTo(OutboundDataRequirement.NONE));
Assert.That(assistantThread.WebAddressesWrittenByModels, Is.Empty);
});
}
}
}
+8 -8
View File
@@ -41,11 +41,11 @@ public sealed class ChatThreadRollbackTests
var rolledBack = thread.RollBackTo(answer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(rolledBack, Is.True, "Two blocks came after the answer, so the rollback removed something.");
Assert.That(Texts(thread), Is.EqualTo(new[] { "First question", "First answer" }), "The hidden prompt goes with the answer to it, although the user never saw it.");
});
}
}
[Test]
@@ -117,7 +117,7 @@ public sealed class ChatThreadRollbackTests
thread.RollBackTo(answer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.AugmentedData, Is.Empty, "Nobody knows whether the retrieved data belongs to a kept or to a removed message, so it must not reach the next system prompt.");
Assert.That(thread.AISelectedDataSources, Is.Empty, "The data sources an agent picked belong to the same retrieval as the data.");
@@ -125,7 +125,7 @@ public sealed class ChatThreadRollbackTests
Assert.That(options.DisableDataSources, Is.False);
Assert.That(options.AutomaticDataSourceSelection, Is.True);
Assert.That(options.PreselectedDataSourceIds, Is.EqualTo(new[] { "handbook" }));
});
}
}
[Test]
@@ -150,12 +150,12 @@ public sealed class ChatThreadRollbackTests
thread.RollBackTo(answer.Content!);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.DataSecurity, Is.EqualTo(DataSourceSecurity.SELF_HOSTED), "The confidential data was seen by this chat, so no cloud provider may continue it.");
Assert.That(thread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH), "The confidence the data demanded stays, although the message which brought it in is gone.");
Assert.That(thread.RequiredOutboundDataRestriction, Is.EqualTo(restriction), "The mail content was read by this chat, so it may still carry it out no further than before.");
});
}
}
[Test]
@@ -194,13 +194,13 @@ public sealed class ChatThreadRollbackTests
private static void AssertUnchanged(ChatThread thread, bool rolledBack, params string[] expectedTexts)
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(rolledBack, Is.False, "Nothing came after the content, so there was nothing to roll back.");
Assert.That(Texts(thread), Is.EqualTo(expectedTexts), "A rollback without anything to remove leaves the blocks alone.");
Assert.That(thread.AugmentedData, Is.EqualTo("A chunk from the handbook"), "A rollback without anything to remove keeps the last retrieval, because it still belongs to the last message.");
Assert.That(thread.AISelectedDataSources, Has.Count.EqualTo(1));
});
}
}
private static ContentBlock Block(ChatRole role, string text, int minute, bool hidden = false) => new()
+12 -12
View File
@@ -29,12 +29,12 @@ public sealed class ChatThreadWebAddressTests
{
var thread = Thread(Block(ChatRole.USER, "Please summarize https://example.org/report for me.", 1));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://example.org/report")), Is.True);
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://EXAMPLE.org/report#summary")), Is.True, "The server gets the same request.");
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://example.org/report?budget=2026")), Is.False, "The query would carry something the user never wrote.");
});
}
}
[Test]
@@ -43,12 +43,12 @@ public sealed class ChatThreadWebAddressTests
// An internal service such as a simple Python web server for the results of a data science run:
var thread = Thread(Block(ChatRole.USER, "The results of run 17 are on http://10.20.30.40:8000/run-17/, please compare them with run 16.", 1));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("http://10.20.30.40:8000/run-17/")), Is.True);
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("http://10.20.30.40:8000/run-16/")), Is.False, "The user named run 16, but not its address.");
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://10.20.30.40:8000/run-17/")), Is.False, "Another scheme is another request.");
});
}
}
[Test]
@@ -89,11 +89,11 @@ public sealed class ChatThreadWebAddressTests
using var rustService = new RustService("1", "unused");
thread.PrepareSystemPrompt(new SettingsManager(NullLogger<SettingsManager>.Instance, rustService));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://handbook.example.org/")), Is.True);
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://intranet.example.org/travel")), Is.True, "The RAG process found it in a data source, the model did not choose it.");
});
}
}
[Test]
@@ -131,11 +131,11 @@ public sealed class ChatThreadWebAddressTests
var thread = Thread(Block(ChatRole.USER, "Make slides of this: the details are on https://example.org/chosen, and our handbook is on https://example.org/handbook.", 1));
thread.RequireRestrictions(ChatRestrictions.NONE.WithWebAddressesWrittenByModelIn("The details are on https://example.org/chosen."));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://example.org/chosen")), Is.False, "A model wrote it, so it could carry anything of the chat it came from.");
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://example.org/handbook")), Is.True, "Another address in the same message still counts.");
});
}
}
[Test]
@@ -151,11 +151,11 @@ public sealed class ChatThreadWebAddressTests
thread.RequireRestrictions(ChatRestrictions.NONE.WithWebAddressesWrittenByModelIn("See https://example.org/chosen and https://example.org/sources/2026."));
thread.RuntimeWebAddressesFromTools.Add(WebAddresses.CreateRequestKey(new Uri("https://example.org/chosen")));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://example.org/chosen")), Is.True, "A tool returned it in this chat, so it was not only a model which wrote it.");
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://example.org/sources/2026")), Is.True, "A document the user attached holds it.");
});
}
}
[Test]
@@ -177,11 +177,11 @@ public sealed class ChatThreadWebAddressTests
copy.RequireRestrictions(ChatRestrictions.NONE.WithWebAddressesWrittenByModelIn("See https://example.org/chosen."));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(copy.IsWebAddressGivenToTheModel(new Uri("https://example.org/chosen")), Is.False);
Assert.That(thread.IsWebAddressGivenToTheModel(new Uri("https://example.org/chosen")), Is.True, "What the copy took over must not reach the chat it was copied from.");
});
}
}
private static ChatThread Thread(params ContentBlock[] blocks) => new() { Blocks = [..blocks] };
@@ -21,13 +21,13 @@ public sealed class ContentBlockApartFromChatTests
var template = new ChatThread { ChatId = Guid.NewGuid(), Blocks = chat.Blocks.Select(block => block.CloneApartFromChat()).ToList() };
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(template.CollectProvenanceIds(), Is.Empty, "The entries belong to the chat and are deleted with it.");
Assert.That(template.Blocks.Select(block => ((ContentText)block.Content!).Text), Is.EqualTo(new[] { "Summarize the minutes.", "The board met on Monday." }));
Assert.That(template.Blocks[0].Content!.FileAttachments.Select(attachment => attachment.FilePath), Is.EqualTo(new[] { "/tmp/minutes.txt" }), "The file is read anew by every chat it reaches.");
Assert.That(chat.CollectProvenanceIds(), Has.Count.EqualTo(2), "The chat itself keeps what it received.");
});
}
}
[Test]
+2 -2
View File
@@ -15,11 +15,11 @@ public sealed class ContentPresentationTests
presentation.Data = JsonSerializer.SerializeToElement(new { heading = "Second" });
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(firstHashCode, Is.Not.Zero);
Assert.That(presentation.GetDataHashCode(), Is.Not.EqualTo(firstHashCode));
});
}
}
[Test]
+24 -24
View File
@@ -53,12 +53,12 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(thread, "You are helpful.", "And of Italy?", null, imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.EqualTo(new[] { "You are helpful.", "What is the capital of France?", "Paris." }));
Assert.That(parts.DraftText, Is.EqualTo("And of Italy?"));
Assert.That(parts.GrowingTexts, Is.Empty, "The draft is a part of its own, and it stands nowhere else.");
});
}
}
[Test]
@@ -75,12 +75,12 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(thread, string.Empty, "a draft", null, imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.EqualTo(new[] { "A question." }));
Assert.That(parts.GrowingTexts, Is.EqualTo(new[] { "The answer so far" }));
Assert.That(parts.DraftText, Is.EqualTo("a draft"));
});
}
}
[Test]
@@ -91,11 +91,11 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(new() { Blocks = [finished] }, string.Empty, string.Empty, null, imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.EqualTo(new[] { "The whole answer." }));
Assert.That(parts.GrowingTexts, Is.Empty);
});
}
}
[Test]
@@ -134,11 +134,11 @@ public sealed class ConversationPartsTests
{
var parts = ConversationParts.Of(null, string.Empty, "Hello", null, imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.Empty);
Assert.That(parts.DraftText, Is.EqualTo("Hello"));
});
}
}
[TestCase("")]
@@ -147,12 +147,12 @@ public sealed class ConversationPartsTests
{
var parts = ConversationParts.Of(null, string.Empty, draft, null, imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.Empty);
Assert.That(parts.GrowingTexts, Is.Empty);
Assert.That(parts.DraftText, Is.Empty);
});
}
}
[Test]
@@ -169,11 +169,11 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(new() { Blocks = [empty] }, string.Empty, string.Empty, null, imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.Empty);
Assert.That(parts.Documents, Is.Empty);
});
}
}
[Test]
@@ -191,11 +191,11 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(new() { Blocks = [block] }, string.Empty, "And this one.", [FileAttachment.FromPath(draft)], imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Documents.Select(document => document.FileName), Is.EqualTo(new[] { "older.txt" }));
Assert.That(parts.DraftDocuments.Select(document => document.FileName), Is.EqualTo(new[] { "draft.txt" }), "Both count, each in the part it belongs to.");
});
}
}
[Test]
@@ -219,11 +219,11 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(null, string.Empty, "Look", [FileAttachment.FromPath(document), FileAttachment.FromPath(image)], imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.DraftDocuments.Select(entry => entry.FileName), Is.EqualTo(new[] { "notes.txt" }));
Assert.That(parts.DraftImages, Is.EqualTo(1));
});
}
}
[Test]
@@ -240,11 +240,11 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(new() { Blocks = [block] }, string.Empty, "And at this.", [FileAttachment.FromPath(waiting)], imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Images, Is.EqualTo(1));
Assert.That(parts.DraftImages, Is.EqualTo(1));
});
}
}
[Test]
@@ -275,12 +275,12 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(new() { Blocks = [running] }, string.Empty, string.Empty, null, imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.Empty);
Assert.That(parts.ToolConversation, Is.EqualTo(new[] { "What the web search found.", "What the page said." }));
Assert.That(parts.GrowingTexts, Is.Empty, "The tool conversation is a part of its own, so that its share can be named.");
});
}
}
[Test]
@@ -314,11 +314,11 @@ public sealed class ConversationPartsTests
var parts = ConversationParts.Of(new() { Blocks = [answered] }, string.Empty, string.Empty, null, imagesAreSent: true, toolDefinitions: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.EqualTo(new[] { "Here is what I found." }));
Assert.That(parts.ToolConversation, Is.Empty);
});
}
}
[Test]
@@ -335,7 +335,7 @@ public sealed class ConversationPartsTests
Tool("read_web_page", "Reads one page.", """{"type":"string"}"""),
]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(parts.Texts, Is.EqualTo(new[]
{
@@ -344,7 +344,7 @@ public sealed class ConversationPartsTests
}));
Assert.That(parts.GrowingTexts, Is.Empty);
});
}
}
[Test]
@@ -131,11 +131,11 @@ public sealed class ConversationTokenTrackerTests
tracker.Start();
await Task.Delay(1_000);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(runs, Is.GreaterThanOrEqualTo(2));
Assert.That(asked, Is.EqualTo(runs));
});
}
}
[Test]
@@ -155,11 +155,11 @@ public sealed class ConversationTokenTrackerTests
var afterDisposal = runs;
await Task.Delay(400);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(afterDisposal, Is.GreaterThan(0), "The tracker never ran, so this proves nothing about stopping it.");
Assert.That(runs, Is.EqualTo(afterDisposal));
});
}
}
[Test]
+6 -6
View File
@@ -64,11 +64,11 @@ public sealed class ConversationTokensTests
ImageLimits = new ImageLimits(8, 100),
};
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(counted.TooManyImages, Is.True);
Assert.That(counted.ImageLimits.MaxInOneMessage, Is.EqualTo(8));
});
}
}
[Test]
@@ -111,11 +111,11 @@ public sealed class ConversationTokensTests
var estimated = reported with { HistoryIsReported = false };
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(reported.UncountedImages, Is.EqualTo(1), "The provider counted the two which were sent.");
Assert.That(estimated.UncountedImages, Is.EqualTo(3), "Without a report, nobody counted any of them.");
});
}
}
[Test]
@@ -133,11 +133,11 @@ public sealed class ConversationTokensTests
ImageLimits = new ImageLimits(8, null),
};
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(counted.UncountedImages, Is.Zero);
Assert.That(counted.TooManyImages, Is.True);
});
}
}
[Test]
@@ -43,12 +43,12 @@ public sealed class FileAttachmentSerializationTests
var json = JsonSerializer.Serialize(template, SettingsManager.JSON_OPTIONS);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(json, Does.Contain(FILE_PATH.Replace(@"\", @"\\")), "The attachment has to reach the JSON, or this test checks nothing.");
Assert.That(json, Does.Contain("chart.png"), "The attachment of the example conversation has to reach the JSON, or this test checks nothing.");
Assert.That(json, Does.Not.Contain("\"Exists\""));
});
}
}
[Test]
@@ -67,11 +67,11 @@ public sealed class FileAttachmentSerializationTests
var read = JsonSerializer.Deserialize<FileAttachment>(LEGACY_JSON, SettingsManager.JSON_OPTIONS)!;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(read.FileName, Is.EqualTo("report.pdf"));
Assert.That(read.FilePath, Is.EqualTo("/tmp/report.pdf"));
Assert.That(read.FileSizeBytes, Is.EqualTo(1024));
});
}
}
}
+22 -22
View File
@@ -27,12 +27,12 @@ public sealed class IContentExtensionsTests
var found = content.TryGetExportMarkdown(out var markdown);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(found, Is.True);
Assert.That(markdown, Does.EndWith(content.Sources.ToExportMarkdown()), "What the chat shows below the answer is what the file holds below it.");
Assert.That(TopLevelBlocksOf(markdown), Is.EqualTo(new[] { "ParagraphBlock", "h1", "h2", "ListBlock" }), "The answer stays a paragraph of its own; the source list starts under its own heading.");
});
}
}
[Test]
@@ -47,11 +47,11 @@ public sealed class IContentExtensionsTests
var found = content.TryGetExportMarkdown(out var markdown);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(found, Is.True);
Assert.That(TopLevelBlocksOf(markdown), Is.EqualTo(new[] { "ParagraphBlock", "Table", "h1", "h2", "ListBlock" }), "The table ends where it ended; the headings below it are not two more rows.");
});
}
}
[Test]
@@ -66,11 +66,11 @@ public sealed class IContentExtensionsTests
var found = content.TryGetExportMarkdown(out var markdown);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(found, Is.True);
Assert.That(TopLevelBlocksOf(markdown), Is.EqualTo(new[] { "ParagraphBlock", "ListBlock", "h1", "h2", "ListBlock" }), "Two lists, not one: the sources do not become the fourth point of the answer.");
});
}
}
[Test]
@@ -86,11 +86,11 @@ public sealed class IContentExtensionsTests
var found = content.TryGetExportMarkdown(out var markdown);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(found, Is.True);
Assert.That(TopLevelBlocksOf(markdown), Is.EqualTo(new[] { "ParagraphBlock", "FencedCodeBlock", "h1", "h2", "ListBlock" }), "The code block is closed for the model, so the sources stand below it instead of inside it.");
});
}
}
[Test]
@@ -101,11 +101,11 @@ public sealed class IContentExtensionsTests
var found = content.TryGetExportMarkdown(out var markdown);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(found, Is.True);
Assert.That(markdown, Is.EqualTo(ANSWER.Trim()), "The everyday case: no heading, no empty line, nothing anybody has to explain.");
});
}
}
[Test]
@@ -115,11 +115,11 @@ public sealed class IContentExtensionsTests
var found = content.TryGetExportMarkdown(out var markdown);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(found, Is.True);
Assert.That(markdown, Is.EqualTo(content.Sources.ToExportMarkdown()), "Nothing above the heading means no empty line above it either.");
});
}
}
[Test]
@@ -132,11 +132,11 @@ public sealed class IContentExtensionsTests
Sources = [TOOL_SOURCE],
};
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(picture.TryGetExportMarkdown(out var markdown), Is.False, "There is no text document for a picture, so the caller hears no and says so.");
Assert.That(markdown, Is.Empty, "A file writer must not put an excuse into the file it writes.");
});
}
}
[Test]
@@ -147,11 +147,11 @@ public sealed class IContentExtensionsTests
var found = content.TryGetMarkdownText(out var markdown);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(found, Is.True);
Assert.That(markdown, Is.EqualTo(ANSWER), "Neither trimmed nor extended: whoever reads a table out of a message wants what the model wrote and nothing else.");
});
}
}
[Test]
@@ -164,11 +164,11 @@ public sealed class IContentExtensionsTests
var tables = FilesOf(content);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(tables, Has.Count.EqualTo(1), "One table in the message, one table offered for it.");
Assert.That(content.ToExportContent(tables[0]), Is.EqualTo(tables[0].Content), "A data table has no column a link list would fit into.");
});
}
}
[Test]
@@ -201,12 +201,12 @@ public sealed class IContentExtensionsTests
var exported = content.ToExportContent(file);
var appended = exported[file.Content.Length..].Trim();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(exported, Does.StartWith(file.Content), "The page stays as the model wrote it.");
Assert.That(appended, Does.StartWith("<!--").And.EndWith("-->"), "Below the page stands one comment and nothing a browser would show.");
Assert.That(appended, Does.Contain(TOOL_SOURCE.URL));
});
}
}
[Test]
@@ -221,12 +221,12 @@ public sealed class IContentExtensionsTests
var exported = content.ToExportContent(file);
var appended = exported[file.Content.Length..].Trim();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(exported, Does.StartWith(file.Content), "The document stays as the model wrote it.");
Assert.That(appended.Split(Environment.NewLine), Has.All.StartWith("%"), "A line LaTeX would read could stop the whole run.");
Assert.That(appended, Does.Contain(TOOL_SOURCE.URL));
});
}
}
[Test]
@@ -92,11 +92,11 @@ public sealed class ListContentBlockExtensionsTests
var messages = await BlocksWithAPicture(this.picturePath).BuildMessagesUsingDirectImageUrlAsync(PictureReadingProvider());
var picture = (messages.Single() as MultimodalMessage)!.Content.OfType<SubContentImageUrl>().Single();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(picture.ImageUrl, Is.EqualTo($"data:image/jpeg;base64,{FakeImageRuntime.IMAGE_BASE64}"));
Assert.That(this.runtime.RequestedPaths, Is.EqualTo(new[] { new FileInfo(this.picturePath).FullName }));
});
}
}
[Test]
@@ -49,11 +49,11 @@ public sealed class ManagedTranscriptAttachmentTests
var chatDirectory = Path.Combine(this.testDirectory, "workspaces", Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
var transcript = WriteTranscript(ManagedTranscriptAttachment.GetTranscriptDirectory(chatDirectory));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(ManagedTranscriptAttachment.TryDeleteOwnedFile(transcript), Is.True);
Assert.That(File.Exists(transcript.FilePath), Is.False);
});
}
}
[Test]
@@ -62,11 +62,11 @@ public sealed class ManagedTranscriptAttachmentTests
var chatDirectory = Path.Combine(this.testDirectory, "workspaces", Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
var file = WriteTranscript(Path.Combine(chatDirectory, "attachments", "images"));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(ManagedTranscriptAttachment.TryDeleteOwnedFile(file), Is.False, "Only the transcript folder belongs to the transcripts.");
Assert.That(File.Exists(file.FilePath), Is.True);
});
}
}
[Test]
@@ -77,11 +77,11 @@ public sealed class ManagedTranscriptAttachmentTests
{
var file = WriteTranscript(ManagedTranscriptAttachment.GetTranscriptDirectory(Path.Combine(outsideDirectory, "tempChats", Guid.NewGuid().ToString())));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(ManagedTranscriptAttachment.TryDeleteOwnedFile(file), Is.False, "The same folders elsewhere belong to somebody else.");
Assert.That(File.Exists(file.FilePath), Is.True);
});
}
}
finally
{
+14 -14
View File
@@ -50,12 +50,12 @@ public sealed class ProvenanceCleanupTests
var result = await CleanUpAsync(store, Locations(keptChatId), LoaderOf(chat), DateTimeOffset.UtcNow.AddMinutes(1));
var entries = await store.Client.GetEntriesAsync([referenced.Value, ofRemovedMessage!.Value, ofDeletedChat!.Value], TOKEN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(entries.Keys, Is.EquivalentTo(new[] { referenced.Value }));
Assert.That(result.Orphaned, Is.EqualTo(1));
Assert.That(result.Unreferenced, Is.EqualTo(1));
});
}
}
[Test]
@@ -83,11 +83,11 @@ public sealed class ProvenanceCleanupTests
var result = await CleanUpAsync(store, Locations(Guid.NewGuid()), UNREADABLE, DateTimeOffset.UtcNow.AddMinutes(1));
var entries = await store.Client.GetEntriesAsync([entry!.Value], TOKEN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(entries, Is.Empty, "No chat on the disk has no id, so nothing would ever remove such an entry.");
Assert.That(result.Orphaned, Is.EqualTo(1));
});
}
}
[Test]
@@ -100,11 +100,11 @@ public sealed class ProvenanceCleanupTests
var result = await CleanUpAsync(store, Locations(chatId), UNREADABLE, DateTimeOffset.UtcNow.AddMinutes(1));
var entries = await store.Client.GetEntriesAsync([entry!.Value], TOKEN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(entries.Keys, Is.EquivalentTo(new[] { entry.Value }), "Nothing tells what an unreadable chat refers to, e.g., one written by a newer version.");
Assert.That(result.Unreadable, Is.EqualTo(1), "The next start has to read it again.");
});
}
}
[Test]
@@ -119,12 +119,12 @@ public sealed class ProvenanceCleanupTests
var result = await CleanUpAsync(store, Locations(chatId), LoaderOf(ChatWith(passageId: null, attachmentId: null, chatId), reads), lastCleanup.AddMinutes(1), lastCleanup, writtenAt: lastCleanup.AddHours(-1));
var entries = await store.Client.GetEntriesAsync([entry!.Value], TOKEN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(reads, Is.Empty, "Reading every chat at every start costs much on a small computer.");
Assert.That(result.Skipped, Is.EqualTo(1));
Assert.That(entries.Keys, Is.EquivalentTo(new[] { entry.Value }), "Skipping a chat never removes anything.");
});
}
}
[Test]
@@ -139,11 +139,11 @@ public sealed class ProvenanceCleanupTests
await CleanUpAsync(store, Locations(chatId), LoaderOf(ChatWith(passageId: null, attachmentId: null, chatId), reads), DateTimeOffset.UtcNow.AddMinutes(1), lastCleanup, writtenAt: lastCleanup.AddHours(-1));
var entries = await store.Client.GetEntriesAsync([previewed!.Value], TOKEN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(reads, Is.EquivalentTo(new[] { chatId }), "The last cleanup had to leave alone what was stored after it began.");
Assert.That(entries, Is.Empty);
});
}
}
[TestCase(60, TestName = "AChatWrittenSinceTheLastCleanupIsRead")]
@@ -160,11 +160,11 @@ public sealed class ProvenanceCleanupTests
await CleanUpAsync(store, Locations(chatId), LoaderOf(ChatWith(passageId: null, attachmentId: null, chatId), reads), lastCleanup.AddMinutes(1), lastCleanup, writtenAt: lastCleanup.AddSeconds(secondsAfterTheLastCleanup));
var entries = await store.Client.GetEntriesAsync([ofRemovedMessage!.Value], TOKEN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(reads, Is.EquivalentTo(new[] { chatId }));
Assert.That(entries, Is.Empty);
});
}
}
[Test]
@@ -192,12 +192,12 @@ public sealed class ProvenanceCleanupTests
var result = await CleanUpAsync(store, Locations(), LoaderOf(ChatWith(passageId: null, attachmentId: null), reads), lastCleanup.AddMinutes(1), lastCleanup, writtenAt: lastCleanup.AddHours(-1));
var entries = await store.Client.GetEntriesAsync([entry!.Value], TOKEN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(entries, Is.Empty, "Telling that a chat no longer exists costs no reading.");
Assert.That(result.Orphaned, Is.EqualTo(1));
Assert.That(reads, Is.Empty);
});
}
}
private static ChatThread ChatWith(Guid? passageId, Guid? attachmentId, Guid? chatId = null) => new()
@@ -25,12 +25,12 @@ public sealed class ChatComposerStateTests
composer.RestoreFromDraft(draft);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(composer.UserInput, Is.EqualTo("Summarize the meeting"));
Assert.That(composer.FileAttachments.Select(attachment => attachment.FilePath), Is.EqualTo(new[] { "/documents/agenda.pdf" }));
Assert.That(composer.HasUserDraft, Is.True, "A chat template must not replace the message the user came back to.");
});
}
}
[Test]
@@ -41,11 +41,11 @@ public sealed class ChatComposerStateTests
composer.RestoreFromDraft(new ChatDraft());
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(composer.UserInput, Is.Empty, "The message of the chat before belongs to that chat.");
Assert.That(composer.HasUserDraft, Is.False, "Nobody started a message in this chat, so a chat template may still fill the composer.");
});
}
}
[Test]
@@ -55,10 +55,10 @@ public sealed class ChatComposerStateTests
composer.ApplyTemplate(new ChatTemplate { PredefinedUserPrompt = "Translate the following text:" });
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(composer.UserInput, Is.EqualTo("Translate the following text:"));
Assert.That(composer.HasUserDraft, Is.False, "Leaving the chat must not warn about a message the user never wrote.");
});
}
}
}
@@ -132,11 +132,11 @@ public sealed class ConfigurationFileExclusionsTests
component.LockField = locked;
await component.ReplaceAsync("bin/;*.tmp");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(component.Draft, Is.EqualTo("*.original"));
Assert.That(component.StoredText, Is.EqualTo("*.original"));
});
}
}
finally
{
@@ -155,11 +155,11 @@ public sealed class ConfigurationFileExclusionsTests
component.Edit("old/;:text:");
await component.ReplaceAsync("bin/;:text:");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(component.Draft, Is.EqualTo("bin/;:text:"));
Assert.That(component.StoredText, Is.EqualTo("*.original"));
});
}
}
finally
{
+10 -10
View File
@@ -24,11 +24,11 @@ public sealed class ContentViewerTests
var shown = ContentViewer.CutAtLineBreak(TEXT, 100, out var cutOffCharacters);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(shown, Is.SameAs(TEXT));
Assert.That(cutOffCharacters, Is.Zero);
});
}
}
[Test]
@@ -38,11 +38,11 @@ public sealed class ContentViewerTests
var shown = ContentViewer.CutAtLineBreak(text, 50, out var cutOffCharacters);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(shown, Is.EqualTo(text));
Assert.That(cutOffCharacters, Is.Zero);
});
}
}
[Test]
@@ -52,11 +52,11 @@ public sealed class ContentViewerTests
var shown = ContentViewer.CutAtLineBreak(TEXT, 25, out var cutOffCharacters);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(shown, Is.EqualTo("| a | b |\n| 1 | 2 |\n"), "The last table row would be torn apart, so it goes as a whole.");
Assert.That(cutOffCharacters, Is.EqualTo(TEXT.Length - shown.Length));
});
}
}
[Test]
@@ -69,11 +69,11 @@ public sealed class ContentViewerTests
const string TEXT = "The budget $\\rho$ is spent on the hardest tokens.";
static bool HasMath(Markdig.MarkdownPipeline pipeline) => Markdig.Markdown.Parse(TEXT, pipeline).Descendants<MathInline>().Any();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(HasMath(Markdown.SAFE_MARKDOWN_PIPELINE), Is.True, "The safe pipeline reads this as math.");
Assert.That(HasMath(Markdown.CONTENT_MARKDOWN_PIPELINE), Is.False, "What AI Studio read from a document stays text.");
});
}
}
[Test]
@@ -83,10 +83,10 @@ public sealed class ContentViewerTests
var shown = ContentViewer.CutAtLineBreak(text, 20, out var cutOffCharacters);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(shown, Has.Length.EqualTo(20));
Assert.That(cutOffCharacters, Is.EqualTo(10));
});
}
}
}
@@ -24,12 +24,12 @@ public sealed class FileExclusionEditorStateTests
{
var state = Create(text);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.FolderText, Is.EqualTo("node_modules/; bin/"));
Assert.That(state.FileText, Is.EqualTo("*.min.js; draft-*"));
Assert.That(state.Text, Is.EqualTo(text));
});
}
}
[Test]
@@ -38,12 +38,12 @@ public sealed class FileExclusionEditorStateTests
var state = Create("bin/;*.min.js;draft-*");
state.EditFolders("obj/\nbuild/");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(FileExclusions.Parse(state.Text), Is.EqualTo(new[] { "obj/", "build/", "*.min.js", "draft-*" }));
Assert.That(state.FileText, Is.EqualTo("*.min.js; draft-*"));
Assert.That(state.Text, Is.EqualTo("obj/; build/; *.min.js; draft-*"));
});
}
}
[Test]
@@ -52,12 +52,12 @@ public sealed class FileExclusionEditorStateTests
var state = Create("bin/;node_modules/;*.min.js");
state.EditFiles("*.wav;*.md");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(FileExclusions.Parse(state.Text), Is.EqualTo(new[] { "bin/", "node_modules/", "*.wav", "*.md" }));
Assert.That(state.FolderText, Is.EqualTo("bin/; node_modules/"));
Assert.That(state.Text, Is.EqualTo("bin/; node_modules/; *.wav; *.md"));
});
}
}
[TestCase("")]
@@ -80,7 +80,7 @@ public sealed class FileExclusionEditorStateTests
state.EditFolders("node_modules/;cmake-build-*/");
var patterns = FileExclusions.Parse(state.Text);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(patterns, Is.EqualTo(new[] { "node_modules/", "cmake-build-*/" }));
Assert.That(FileExclusions.IsValid(state.Text), Is.True);
@@ -88,7 +88,7 @@ public sealed class FileExclusionEditorStateTests
Assert.That(FileExclusions.ExcludesFolder("node_modules", patterns), Is.True);
Assert.That(FileExclusions.ExcludesFolder("cmake-build-debug", patterns), Is.True);
Assert.That(FileExclusions.ExcludesFile("node_modules", patterns), Is.False);
});
}
}
[TestCase(":text:")]
@@ -103,11 +103,11 @@ public sealed class FileExclusionEditorStateTests
else
state.EditFolders("bin/");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(FileExclusions.Parse(state.Text), Does.Contain(invalidRule));
Assert.That(FileExclusions.IsValid(state.Text), Is.False);
});
}
}
[TestCase(":text:")]
@@ -117,11 +117,11 @@ public sealed class FileExclusionEditorStateTests
var state = Create(string.Empty);
state.EditFolders(selector);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.Text, Is.EqualTo(selector));
Assert.That(FileExclusions.IsValid(state.Text), Is.False);
});
}
}
[Test]
@@ -156,12 +156,12 @@ public sealed class FileExclusionEditorStateTests
else
state.RestoreFiles(DEFAULTS);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.FolderText, Is.EqualTo(folders ? "bin/; node_modules/" : "latest/; "));
Assert.That(state.FileText, Is.EqualTo(folders ? "*.latest; " : "*.tmp; *.bak"));
Assert.That(state.Text, Is.EqualTo(folders ? "bin/; node_modules/; *.latest" : "latest/; *.tmp; *.bak"));
});
}
}
[TestCase(true)]
@@ -187,11 +187,11 @@ public sealed class FileExclusionEditorStateTests
else
state.RestoreFiles(FileExclusions.DEFAULT_PATTERNS);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(FileExclusions.Parse(state.Text), Does.Contain(invalidRule));
Assert.That(FileExclusions.IsValid(state.Text), Is.False);
});
}
}
[Test]
@@ -199,13 +199,13 @@ public sealed class FileExclusionEditorStateTests
{
var state = Create("bin/;*.min.js");
state.EditFiles("obj/;*.wav;draft-*");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.IsValid, Is.False);
Assert.That(state.Text, Is.EqualTo("bin/; obj/; *.wav; draft-*"));
Assert.That(state.FolderText, Is.EqualTo("bin/"));
Assert.That(state.FileText, Is.EqualTo("obj/;*.wav;draft-*"));
});
}
}
[TestCase(true)]
@@ -230,12 +230,12 @@ public sealed class FileExclusionEditorStateTests
state.Receive(earlierValue);
state.Receive(state.Text);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.FolderText, Is.EqualTo("obj/; build/; "));
Assert.That(state.FileText, Is.EqualTo("*.wav; draft-*; "));
Assert.That(state.Text, Is.EqualTo("obj/; build/; *.wav; draft-*"));
});
}
}
[Test]
@@ -247,11 +247,11 @@ public sealed class FileExclusionEditorStateTests
state.EditFiles("*.latest");
state.Receive(earlierValue);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.Text, Is.EqualTo("*.latest"));
Assert.That(state.FileText, Is.EqualTo("*.latest"));
});
}
}
[Test]
@@ -271,11 +271,11 @@ public sealed class FileExclusionEditorStateTests
state.EditFiles("*.latest");
state.Receive("policy/;*.secret");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.FolderText, Is.EqualTo("policy/"));
Assert.That(state.FileText, Is.EqualTo("*.secret"));
});
}
}
[Test]
@@ -311,7 +311,7 @@ public sealed class FileExclusionEditorStateTests
state.EditFolders(":text:");
state.Reset(FileExclusions.DEFAULT_PATTERNS);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.Text, Is.EqualTo(FileExclusions.DEFAULT_PATTERNS));
Assert.That(FileExclusions.IsValid(state.Text), Is.True);
@@ -319,7 +319,7 @@ public sealed class FileExclusionEditorStateTests
Assert.That(state.FileText, Does.Contain("*.exe"));
Assert.That(state.FolderText, Does.Not.Contain("\n").And.Not.Contain("\r"));
Assert.That(state.FileText, Does.Not.Contain("\n").And.Not.Contain("\r"));
});
}
}
[Test]
@@ -341,14 +341,14 @@ public sealed class FileExclusionEditorStateTests
var state = Create("bin/;*.tmp");
state.EditFolders(pattern);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.IsValid, Is.False);
Assert.That(state.FolderText, Is.EqualTo(pattern));
Assert.That(FileExclusions.Parse(state.Text), Does.Contain(pattern));
Assert.That(FileExclusions.Parse(state.Text), Does.Not.Contain(pattern + "/"));
Assert.That(state.FileText, Is.EqualTo("*.tmp"));
});
}
}
[Test]
@@ -360,12 +360,12 @@ public sealed class FileExclusionEditorStateTests
state.EditFiles("*.bak;obj/");
state.Receive(earlierValue);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(state.IsValid, Is.False);
Assert.That(state.FileText, Is.EqualTo("*.bak;obj/"));
Assert.That(state.FolderText, Is.EqualTo("bin/"));
});
}
}
private static FileExclusionEditorState Create(string text)
@@ -68,11 +68,11 @@ public sealed class FileExclusionEditorTests
await editor.EditAsync(folders ? "*.keep;wrong/" : "*.wrong", folders: !folders);
await editor.ResetAsync(folders);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(published, Is.Empty);
Assert.That(resets, Is.Empty);
});
}
await editor.EditAsync(folders ? "*.keep" : "keep/", folders: !folders);
Assert.That(published, Is.EqualTo(new[] { folders ? "bin/; *.keep" : "keep/; *.tmp" }));
@@ -90,11 +90,11 @@ public sealed class FileExclusionEditorTests
await editor.EditAsync(folders ? "*.wrong" : "wrong/", folders);
await editor.ResetAsync(folders);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(published, Is.Empty);
Assert.That(resets, Is.EqualTo(new[] { folders ? "bin/; *.custom" : "custom/; *.tmp" }));
});
}
}
[Test]
@@ -114,11 +114,11 @@ public sealed class FileExclusionEditorTests
await editor.ResetAsync(folders: true);
await editor.ResetAsync(folders: false);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(published, Is.Empty);
Assert.That(resets, Is.Empty);
});
}
editor.SetParameters(new() { [nameof(FileExclusionEditor.ResetRequested)] = default(EventCallback<string>) });
await editor.ResetAsync(folders: true);
@@ -140,11 +140,11 @@ public sealed class FileExclusionEditorTests
await editor.EditAsync("*.wrong", folders: true);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(editor.IsValid, Is.False);
Assert.That(published, Is.Empty);
});
}
await editor.EditAsync("custom/", folders: true);
Assert.That(editor.IsValid, Is.True);
@@ -27,13 +27,13 @@ public sealed class MailFolderPickerTests
Folder("Archive", specialUse: MailFolderSpecialUse.ARCHIVE),
Folder("INBOX", isInbox: true));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(root.Value, Is.Empty, "The root does not stand for the whole mailbox.");
Assert.That(root.Text, Is.EqualTo(WHOLE_MAILBOX));
Assert.That(ValuesOf(root), Is.EqualTo(new[] { "INBOX", "Archive", "Projects" }), "The top level is not ordered with the inbox first.");
Assert.That(ValuesOf(Child(root, "Projects")), Is.EqualTo(new[] { "Projects/Alpha" }), "The subfolder did not land below its parent.");
});
}
}
[Test]
@@ -44,11 +44,11 @@ public sealed class MailFolderPickerTests
Folder("INBOX.Sent", "INBOX", separator: '.', specialUse: MailFolderSpecialUse.SENT),
Folder("INBOX.Drafts", "INBOX", separator: '.', specialUse: MailFolderSpecialUse.DRAFTS));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(ValuesOf(root), Is.EqualTo(new[] { "INBOX" }));
Assert.That(ValuesOf(Child(root, "INBOX")), Is.EqualTo(new[] { "INBOX.Drafts", "INBOX.Sent" }));
});
}
}
[Test]
@@ -85,13 +85,13 @@ public sealed class MailFolderPickerTests
};
var root = MailFolderPicker.BuildTree(folders, "Projects/Alpha/Reports", WHOLE_MAILBOX).Single();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(root.Expanded, Is.True, "The whole mailbox is collapsed.");
Assert.That(Child(root, "Projects").Expanded, Is.True, "The way to the picked folder is collapsed.");
Assert.That(Child(Child(root, "Projects"), "Projects/Alpha").Expanded, Is.True, "The way to the picked folder is collapsed.");
Assert.That(Child(root, "Private").Expanded, Is.False, "A folder off the way to the picked folder is expanded.");
});
}
}
[TestCase(MailFolderSpecialUse.NONE, ExpectedResult = true)]
+2 -2
View File
@@ -37,7 +37,7 @@ public sealed class CapabilityTests
{
var bits = new Dictionary<ulong, Capability>();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
foreach (var capability in Enum.GetValues<Capability>())
{
@@ -52,7 +52,7 @@ public sealed class CapabilityTests
bits[value] = capability;
}
});
}
}
[Test]
+6 -6
View File
@@ -53,11 +53,11 @@ public sealed class ContextWindowRuleTests
{
var window = provider.GetModelProfile(new Model(modelId, null)).Context;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(window.IsKnown, Is.True);
Assert.That(window.DefaultTokens, Is.EqualTo(tokens));
});
}
}
[Test]
@@ -70,11 +70,11 @@ public sealed class ContextWindowRuleTests
//
var profile = ModelRegistry.Shared.Profile(LLMProviders.OPEN_AI, "gpt-5.3");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Context.IsKnown, Is.False);
Assert.That(profile.Has(Capability.FUNCTION_CALLING), Is.True, "Everything else it does inherit.");
});
}
}
[Test]
@@ -100,10 +100,10 @@ public sealed class ContextWindowRuleTests
//
var profile = ModelRegistry.Shared.Profile(LLMProviders.SELF_HOSTED, "some-model-nobody-wrote-a-rule-for");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Context.IsKnown, Is.False);
Assert.That(profile.Context.DefaultTokens, Is.Zero, "And the number next to it is meaningless, which is why nothing may read it without asking first.");
});
}
}
}
+2 -2
View File
@@ -41,7 +41,7 @@ public sealed class CorpusTests
[Test]
public void EveryKnownWrongAnswerSaysWhyAndWhereThatCanBeChecked()
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
foreach (var change in ExpectedChanges.ENTRIES)
{
@@ -49,7 +49,7 @@ public sealed class CorpusTests
Assert.That(change.Source, Is.Not.Empty, $"{change.Provider} {change.ModelId} does not say where that can be checked.");
Assert.That(change.AnswerWanted, Is.Not.Empty, $"{change.Provider} {change.ModelId} does not say what the answer should be.");
}
});
}
}
[Test]
@@ -51,12 +51,12 @@ public sealed class ModelPatternLiteralAnalyzerTests
{
var reported = await AnalyzeAsync(statements);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(reported, Has.Count.EqualTo(1));
Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0013"));
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain($"write it as \"{expectedSpelling}\""));
});
}
}
[Test]
@@ -64,11 +64,11 @@ public sealed class ModelPatternLiteralAnalyzerTests
{
var reported = await AnalyzeAsync("""builder.Rule("___");""");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(reported, Has.Count.EqualTo(1));
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain("nothing of it survives"));
});
}
}
[Test]
@@ -97,12 +97,12 @@ public sealed class ModelRegistryGeneratorTests
{
var generated = Generate(TWO_FAMILIES_AND_A_HOST, out _, out _);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(generated, Does.Contain("new global::Sample.FirstFamily()"));
Assert.That(generated, Does.Contain("new global::Sample.SecondFamily()"), "A family which inherits through another class is still a family.");
Assert.That(generated, Does.Contain("new global::Sample.SampleHost()"));
});
}
}
[Test]
@@ -142,11 +142,11 @@ public sealed class ModelRegistryGeneratorTests
//
var generated = Generate("namespace Sample;\n\npublic sealed class NothingToDoWithModels;", out var updated, out _);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(generated, Does.Contain("public static class ModelRegistrations"));
Assert.That(CompilationHarness.ErrorsOf(updated), Is.Empty);
});
}
}
[Test]
@@ -155,12 +155,12 @@ public sealed class ModelRegistryGeneratorTests
var generated = Generate(A_FAMILY_NEEDING_AN_ARGUMENT, out _, out var diagnostics);
var reported = diagnostics.Where(diagnostic => diagnostic.Id is "MDR001").ToList();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(reported, Has.Count.EqualTo(1));
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain("DemandingFamily"));
Assert.That(generated, Does.Not.Contain("DemandingFamily"));
});
}
}
[Test]
@@ -170,11 +170,11 @@ public sealed class ModelRegistryGeneratorTests
// The tests above run the generator by hand. This one asks whether it also ran while the app
// was built, which is a different question and the one that actually matters.
//
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(ModelRegistrations.CreateFamilies(), Is.Not.Null);
Assert.That(ModelRegistrations.CreateHosts(), Is.Not.Null);
});
}
}
private static string Generate(string source, out Compilation updated, out IReadOnlyList<Diagnostic> diagnostics)
+22 -22
View File
@@ -20,12 +20,12 @@ public sealed class HostNamingTests
{
var taken = HostNaming.TrySplitOrganization(new ModelId("anthropic/claude-opus-5"), out var inner, out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.True);
Assert.That(inner.Original, Is.EqualTo("claude-opus-5"));
Assert.That(vendor, Is.EqualTo(ModelVendor.ANTHROPIC));
});
}
}
[Test]
@@ -37,18 +37,18 @@ public sealed class HostNamingTests
//
var taken = HostNaming.TrySplitOrganization(new ModelId("azure/gpt-5.6"), out var inner, out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.True);
Assert.That(inner.Original, Is.EqualTo("gpt-5.6"));
Assert.That(vendor, Is.Null);
});
}
}
[Test]
public void AnOrganizationIsRecognizedWhicheverWayTheHostSpellsIt()
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(HostNaming.VendorOfOrganization("meta-llama"), Is.EqualTo(ModelVendor.META));
Assert.That(HostNaming.VendorOfOrganization("Qwen"), Is.EqualTo(ModelVendor.ALIBABA));
@@ -57,7 +57,7 @@ public sealed class HostNamingTests
Assert.That(HostNaming.VendorOfOrganization("zai"), Is.EqualTo(ModelVendor.Z_AI));
Assert.That(HostNaming.VendorOfOrganization("minimaxi"), Is.EqualTo(ModelVendor.MINIMAX));
Assert.That(HostNaming.VendorOfOrganization("somebody-else"), Is.EqualTo(ModelVendor.UNKNOWN));
});
}
}
[Test]
@@ -65,12 +65,12 @@ public sealed class HostNamingTests
{
var taken = HostNaming.TrySplitOrganization(new ModelId("llama-3.3-70b-versatile"), out var inner, out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.False);
Assert.That(inner.Original, Is.EqualTo("llama-3.3-70b-versatile"));
Assert.That(vendor, Is.Null);
});
}
}
[Test]
@@ -82,11 +82,11 @@ public sealed class HostNamingTests
//
var taken = HostNaming.TrySplitOrganization(new ModelId("accounts/fireworks/models/llama-v3p1-405b-instruct"), out var inner, out _);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.True);
Assert.That(inner.Original, Is.EqualTo("fireworks/models/llama-v3p1-405b-instruct"));
});
}
}
[Test]
@@ -94,11 +94,11 @@ public sealed class HostNamingTests
{
var taken = HostNaming.TrySplitOrganization(new ModelId("openai/"), out var inner, out _);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.False);
Assert.That(inner.Original, Is.EqualTo("openai/"));
});
}
}
[Test]
@@ -106,11 +106,11 @@ public sealed class HostNamingTests
{
var taken = HostNaming.TryStripRoutingSuffix(new ModelId("google/gemma-4-31B-it:novita"), out var inner);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.True);
Assert.That(inner.Original, Is.EqualTo("google/gemma-4-31B-it"));
});
}
}
[Test]
@@ -118,11 +118,11 @@ public sealed class HostNamingTests
{
var taken = HostNaming.TryStripRegionSuffix(new ModelId("bedrock/claude-opus-5@eu-central-1"), out var inner);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.True);
Assert.That(inner.Original, Is.EqualTo("bedrock/claude-opus-5"));
});
}
}
[TestCase("bedrock/claude-opus-5", TestName = "A name without an at sign has no region to take off")]
@@ -131,11 +131,11 @@ public sealed class HostNamingTests
{
var taken = HostNaming.TryStripRegionSuffix(new ModelId(modelId), out var inner);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.False);
Assert.That(inner.Original, Is.EqualTo(modelId));
});
}
}
[Test]
@@ -143,11 +143,11 @@ public sealed class HostNamingTests
{
var taken = HostNaming.TryStripMenuPosition(new ModelId("10 - Muse Glimmer 30b - the newest META model"), out var inner);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.True);
Assert.That(inner.Original, Is.EqualTo("Muse Glimmer 30b - the newest META model"));
});
}
}
[TestCase("70b-instruct", TestName = "A number the model is named after is not a menu position")]
@@ -158,10 +158,10 @@ public sealed class HostNamingTests
{
var taken = HostNaming.TryStripMenuPosition(new ModelId(modelId), out var inner);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(taken, Is.False);
Assert.That(inner.Original, Is.EqualTo(modelId));
});
}
}
}
@@ -49,12 +49,12 @@ public sealed class ModelHostIndexTests
{
var index = ModelHostIndex.Build([new SplittingHost()]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(index.ProvidersWithoutAHost, Does.Contain(LLMProviders.ANTHROPIC));
Assert.That(index.ProvidersWithoutAHost, Does.Not.Contain(LLMProviders.OPEN_ROUTER));
Assert.That(index.ProvidersWithoutAHost, Does.Not.Contain(LLMProviders.NONE), "Nobody can configure it, so nobody has to write a host for it.");
});
}
}
[Test]
@@ -63,12 +63,12 @@ public sealed class ModelHostIndexTests
var index = ModelHostIndex.Build([new SplittingHost()]);
var unwrapped = index.Unwrap(new ModelId("anthropic/claude-opus-5"), LLMProviders.ANTHROPIC, out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(index.Of(LLMProviders.ANTHROPIC), Is.Null);
Assert.That(unwrapped.Original, Is.EqualTo("anthropic/claude-opus-5"));
Assert.That(vendor, Is.Null);
});
}
}
[Test]
@@ -82,11 +82,11 @@ public sealed class ModelHostIndexTests
var profile = new ModelProfile { Capabilities = Capability.TEXT_INPUT | Capability.RESPONSES_API };
var throughTheProvider = index.ApplyTransport(profile, LLMProviders.ANTHROPIC);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(throughTheProvider.Has(Capability.RESPONSES_API), Is.False);
Assert.That(throughTheProvider.Has(Capability.CHAT_COMPLETION_API), Is.True);
});
}
}
[Test]
+28 -28
View File
@@ -47,11 +47,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.OPEN_ROUTER, "anthropic/claude-opus-5", out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo("claude-opus-5"));
Assert.That(vendor, Is.EqualTo(ModelVendor.ANTHROPIC));
});
}
}
[Test]
@@ -59,11 +59,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.HUGGINGFACE, "openai/gpt-oss-120b:fireworks-ai", out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo("gpt-oss-120b"));
Assert.That(vendor, Is.EqualTo(ModelVendor.OPEN_AI), "OpenAI published the weights, whoever is serving them today.");
});
}
}
[Test]
@@ -71,11 +71,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.HUGGINGFACE, "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo("DeepSeek-R1-Distill-Qwen-32B"));
Assert.That(vendor, Is.EqualTo(ModelVendor.DEEP_SEEK));
});
}
}
[Test]
@@ -83,11 +83,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.REQUESTY, "bedrock/claude-opus-5@eu-central-1", out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo("claude-opus-5"));
Assert.That(vendor, Is.Null, "Bedrock runs the model; it did not build it.");
});
}
}
[Test]
@@ -95,11 +95,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.REQUESTY, "claude-opus-5@eu", out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo("claude-opus-5"));
Assert.That(vendor, Is.Null);
});
}
}
[Test]
@@ -107,11 +107,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.REQUESTY, "openai/gpt-6-sol:flex", out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo("gpt-6-sol"));
Assert.That(vendor, Is.EqualTo(ModelVendor.OPEN_AI));
});
}
}
[TestCase("novita/qwen/qwen3.8-max", "qwen3.8-max", ModelVendor.ALIBABA, TestName = "Behind the cloud, the organization from the hub names the vendor")]
@@ -120,11 +120,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.REQUESTY, modelId, out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo(expectedModel));
Assert.That(vendor, Is.EqualTo(expectedVendor));
});
}
}
[Test]
@@ -132,11 +132,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.FIREWORKS, "accounts/fireworks/models/llama-v3p1-405b-instruct", out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo("llama-v3p1-405b-instruct"));
Assert.That(vendor, Is.Null, "None of the three path segments names a vendor.");
});
}
}
[Test]
@@ -152,11 +152,11 @@ public sealed class ModelHostTests
{
var unwrapped = Unwrap(LLMProviders.SELF_HOSTED, "meta-llama/Llama-3.3-70B-Instruct", out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo("Llama-3.3-70B-Instruct"));
Assert.That(vendor, Is.EqualTo(ModelVendor.META));
});
}
}
[Test]
@@ -184,11 +184,11 @@ public sealed class ModelHostTests
//
var unwrapped = Unwrap(LLMProviders.SELF_HOSTED, modelId, out var vendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(unwrapped.Original, Is.EqualTo(expectedModel));
Assert.That(vendor, Is.EqualTo(expectedVendor));
});
}
}
[Test]
@@ -203,13 +203,13 @@ public sealed class ModelHostTests
var atItsVendor = new ModelProfile { Capabilities = Capability.TEXT_INPUT | Capability.FUNCTION_CALLING | Capability.RESPONSES_API };
var throughTheReseller = Transport(LLMProviders.GWDG, atItsVendor);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(Unwrap(LLMProviders.GWDG, "claude-sonnet-5", out _).Original, Is.EqualTo("claude-sonnet-5"));
Assert.That(throughTheReseller.Has(Capability.FUNCTION_CALLING), Is.True);
Assert.That(throughTheReseller.Has(Capability.RESPONSES_API), Is.False);
Assert.That(throughTheReseller.Has(Capability.CHAT_COMPLETION_API), Is.True);
});
}
}
[Test]
@@ -221,11 +221,11 @@ public sealed class ModelHostTests
.Where(host => host.ApplyTransport(withBothApis).Has(Capability.RESPONSES_API))
.Select(host => host.GetType().Name);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(Transport(LLMProviders.OPEN_AI, withBothApis).Has(Capability.RESPONSES_API), Is.True);
Assert.That(elsewhere, Is.Empty, "The app sends a Responses API request from exactly one place.");
});
}
}
[Test]
@@ -238,22 +238,22 @@ public sealed class ModelHostTests
var embedding = new ModelProfile { Capabilities = Capability.EMBEDDING };
var throughAGateway = Transport(LLMProviders.OPEN_ROUTER, embedding);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(throughAGateway.Has(Capability.EMBEDDING), Is.True);
Assert.That(throughAGateway.HasAny(Capability.CHAT_COMPLETION_API | Capability.RESPONSES_API), Is.False);
});
}
}
[Test]
public void ANameWithoutAWrappingComesBackAsItWas()
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(Unwrap(LLMProviders.OPEN_AI, "gpt-5.6", out _).Original, Is.EqualTo("gpt-5.6"));
Assert.That(Unwrap(LLMProviders.GROQ, "llama-3.3-70b-versatile", out _).Original, Is.EqualTo("llama-3.3-70b-versatile"));
Assert.That(Unwrap(LLMProviders.LITE_LLM, "the-fast-one", out _).Original, Is.EqualTo("the-fast-one"));
});
}
}
private static ModelId Unwrap(LLMProviders provider, string modelId, out ModelVendor? declaredVendor) => INDEX.Unwrap(new ModelId(modelId), provider, out declaredVendor);
+8 -8
View File
@@ -37,12 +37,12 @@ public sealed class ImageLimitRuleTests
{
var limits = provider.GetModelProfile(new Model(modelId, null)).Images;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(limits.IsKnown, Is.True);
Assert.That(limits.MaxPerRequest, Is.EqualTo(perRequest));
Assert.That(limits.MaxPerMessage, Is.Null, "Neither vendor states a per-message limit, and inventing one would be a ceiling nobody wrote.");
});
}
}
[Test]
@@ -57,13 +57,13 @@ public sealed class ImageLimitRuleTests
var smallWindow = ModelRegistry.Shared.Profile(LLMProviders.ANTHROPIC, "claude-opus-4-1");
var largeWindow = ModelRegistry.Shared.Profile(LLMProviders.ANTHROPIC, "claude-opus-4-6");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(smallWindow.Context.DefaultTokens, Is.EqualTo(200_000));
Assert.That(smallWindow.Images.MaxPerRequest, Is.EqualTo(100));
Assert.That(largeWindow.Context.DefaultTokens, Is.EqualTo(1_000_000));
Assert.That(largeWindow.Images.MaxPerRequest, Is.EqualTo(600));
});
}
}
[Test]
@@ -76,11 +76,11 @@ public sealed class ImageLimitRuleTests
//
var profile = ModelRegistry.Shared.Profile(LLMProviders.SELF_HOSTED, "some-model-nobody-wrote-a-rule-for");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Images.IsKnown, Is.False);
Assert.That(profile.Images.MaxInOneMessage, Is.Null);
});
}
}
[Test]
@@ -93,11 +93,11 @@ public sealed class ImageLimitRuleTests
//
var profile = ModelRegistry.Shared.Profile(LLMProviders.OPEN_AI, "gpt-5.1");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True, "It does read several images.");
Assert.That(profile.Images.IsKnown, Is.False, "How many, nobody said.");
});
}
}
[Test]
+12 -12
View File
@@ -35,11 +35,11 @@ public sealed class ListedModelsTests
{
var listed = new ListedModels();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(listed.Of(ONE_MACHINE, MODEL), Is.EqualTo(ModelListing.NOTHING));
Assert.That(listed.Of(ONE_MACHINE, MODEL).IsKnown, Is.False);
});
}
}
[Test]
@@ -53,11 +53,11 @@ public sealed class ListedModelsTests
listed.Report(ONE_MACHINE, [new(MODEL, ContextWindow.Of(32_768))]);
listed.Report(ANOTHER_MACHINE, [new(MODEL, ContextWindow.Of(8_192))]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(listed.Of(ONE_MACHINE, MODEL).Context.DefaultTokens, Is.EqualTo(32_768));
Assert.That(listed.Of(ANOTHER_MACHINE, MODEL).Context.DefaultTokens, Is.EqualTo(8_192));
});
}
}
[Test]
@@ -67,11 +67,11 @@ public sealed class ListedModelsTests
listed.Report(ONE_MACHINE, [new(MODEL, ContextWindow.Of(32_768)), new("gemma3-27b", ContextWindow.Of(16_384))]);
listed.Report(ONE_MACHINE, [new("gemma3-27b", ContextWindow.Of(16_384))]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(listed.Of(ONE_MACHINE, MODEL), Is.EqualTo(ModelListing.NOTHING), "The engine was restarted without it, so nothing is known about it any more.");
Assert.That(listed.Of(ONE_MACHINE, "gemma3-27b").Context.DefaultTokens, Is.EqualTo(16_384));
});
}
}
[Test]
@@ -105,11 +105,11 @@ public sealed class ListedModelsTests
var listed = new ListedModels();
listed.Report(string.Empty, [new(MODEL, ContextWindow.Of(32_768))]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(listed.Of(string.Empty, MODEL), Is.EqualTo(ModelListing.NOTHING));
Assert.That(listed.Of(ONE_MACHINE, MODEL), Is.EqualTo(ModelListing.NOTHING), "One nameless report does not become every machine's answer.");
});
}
}
[Test]
@@ -126,11 +126,11 @@ public sealed class ListedModelsTests
{
var after = new ModelListing(MODEL, ContextWindow.Of(32_768)).ApplyTo(WHAT_THE_RULES_SAY);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(after.Context.DefaultTokens, Is.EqualTo(32_768));
Assert.That(after.Context.RaisableToTokens, Is.Null, "What the weights could be raised to is not a number anybody reaches without restarting this engine.");
});
}
}
[Test]
@@ -138,12 +138,12 @@ public sealed class ListedModelsTests
{
var after = new ModelListing(MODEL, ContextWindow.Of(32_768)).ApplyTo(WHAT_THE_RULES_SAY);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(after.Capabilities, Is.EqualTo(WHAT_THE_RULES_SAY.Capabilities));
Assert.That(after.Images, Is.EqualTo(WHAT_THE_RULES_SAY.Images));
Assert.That(after.Kind, Is.EqualTo(WHAT_THE_RULES_SAY.Kind));
});
}
}
[Test]
@@ -21,11 +21,11 @@ public sealed class MatchPatternTests
var onAlibaba = new MatchPattern { Kind = MatchKind.SEGMENT, Text = "qwq", OnlyOn = LLMProviders.ALIBABA_CLOUD };
var name = new ModelId("qwq-32b");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(onAlibaba.Matches(name, LLMProviders.ALIBABA_CLOUD, ModelVendor.UNKNOWN), Is.True);
Assert.That(onAlibaba.Matches(name, LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN), Is.False);
});
}
}
[Test]
@@ -34,11 +34,11 @@ public sealed class MatchPatternTests
var fromAnthropic = new MatchPattern { Kind = MatchKind.SEGMENT, Text = "claude", OnlyFrom = ModelVendor.ANTHROPIC };
var name = new ModelId("claude-sonnet-4-0");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(fromAnthropic.Matches(name, LLMProviders.LITE_LLM, ModelVendor.ANTHROPIC), Is.True);
Assert.That(fromAnthropic.Matches(name, LLMProviders.LITE_LLM, ModelVendor.UNKNOWN), Is.False);
});
}
}
[Test]
@@ -46,12 +46,12 @@ public sealed class MatchPatternTests
{
var withVision = new MatchPattern { Kind = MatchKind.SEGMENT, Text = "qwen3.8", AlsoContains = ["vl"] };
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(withVision.Matches(new ModelId("qwen3.8-27b-vl"), LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN), Is.True);
Assert.That(withVision.Matches(new ModelId("qwen3.8-27b"), LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN), Is.False);
Assert.That(withVision.Matches(new ModelId("qwen3.8-27b-vllm"), LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN), Is.False, "\"vl\" inside another name part is not the vision variant.");
});
}
}
[Test]
@@ -62,11 +62,11 @@ public sealed class MatchPatternTests
//
var withoutTools = new MatchPattern { Kind = MatchKind.SEGMENT, Text = "salamandra", NotContains = ["tools"] };
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(withoutTools.Matches(new ModelId("salamandra-7b-instruct"), LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN), Is.True);
Assert.That(withoutTools.Matches(new ModelId("salamandra-7b-instruct-tools"), LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN), Is.False);
});
}
}
[TestCase("gpt-5.1", true)]
@@ -29,11 +29,11 @@ public sealed class ModelFamilyIndexTests
var distill = Selector("deepseek-r1", MatchKind.SEGMENT, new() { Adds = Capability.TEXT_INPUT | Capability.FUNCTION_CALLING, Reasoning = ReasoningSupport.ALWAYS });
var name = new ModelId("deepseek-r1-distill-llama-70b");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(ModelFamilyIndex.Build([llama, distill]).Explain(name, ANY_PROVIDER, ModelVendor.UNKNOWN).Selector, Is.SameAs(distill));
Assert.That(ModelFamilyIndex.Build([distill, llama]).Explain(name, ANY_PROVIDER, ModelVendor.UNKNOWN).Selector, Is.SameAs(distill), "The order the rules arrive in must not change the answer.");
});
}
}
[Test]
@@ -47,11 +47,11 @@ public sealed class ModelFamilyIndexTests
var chat = Selector("gpt-5-chat", MatchKind.PREFIX, new() { Adds = Capability.TEXT_INPUT, Reasoning = ReasoningSupport.NONE });
var index = ModelFamilyIndex.Build([reasoning, chat]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(index.Resolve(new ModelId("gpt-5-chat-latest"), ANY_PROVIDER, ModelVendor.UNKNOWN).Reasoning, Is.EqualTo(ReasoningSupport.NONE));
Assert.That(index.Resolve(new ModelId("gpt-5-pro"), ANY_PROVIDER, ModelVendor.UNKNOWN).Reasoning, Is.EqualTo(ReasoningSupport.ALWAYS));
});
}
}
[Test]
@@ -79,11 +79,11 @@ public sealed class ModelFamilyIndexTests
var index = ModelFamilyIndex.Build([openWeights, commercial]);
var name = new ModelId("qwq-32b");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(index.Explain(name, LLMProviders.ALIBABA_CLOUD, ModelVendor.UNKNOWN).Selector, Is.SameAs(commercial));
Assert.That(index.Explain(name, LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN).Selector, Is.SameAs(openWeights));
});
}
}
[Test]
@@ -98,11 +98,11 @@ public sealed class ModelFamilyIndexTests
var baseCheckpoint = Modifier("base", MatchKind.SEGMENT, new() { Removes = Capability.FUNCTION_CALLING });
var index = ModelFamilyIndex.Build([family, baseCheckpoint]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(index.Resolve(new ModelId("llama-3.3-70b"), ANY_PROVIDER, ModelVendor.UNKNOWN).Has(Capability.FUNCTION_CALLING), Is.True);
Assert.That(index.Resolve(new ModelId("llama-3.3-70b-base"), ANY_PROVIDER, ModelVendor.UNKNOWN).Has(Capability.FUNCTION_CALLING), Is.False);
});
}
}
[Test]
@@ -113,11 +113,11 @@ public sealed class ModelFamilyIndexTests
var narrow = Modifier("instruct-nano", MatchKind.SEGMENT, new() { Removes = Capability.FUNCTION_CALLING });
var resolution = ModelFamilyIndex.Build([narrow, broad, family]).Explain(new ModelId("llama-3.3-instruct-nano"), ANY_PROVIDER, ModelVendor.UNKNOWN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(resolution.Modifiers.Select(modifier => modifier.Pattern.Text), Is.EqualTo(new[] { "instruct", "instruct-nano" }));
Assert.That(resolution.Profile.Has(Capability.FUNCTION_CALLING), Is.False);
});
}
}
[Test]
@@ -154,12 +154,12 @@ public sealed class ModelFamilyIndexTests
var oneWay = ModelFamilyIndex.Build([one, other]).Explain(name, ANY_PROVIDER, ModelVendor.UNKNOWN);
var otherWay = ModelFamilyIndex.Build([other, one]).Explain(name, ANY_PROVIDER, ModelVendor.UNKNOWN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(oneWay.IsAmbiguous, Is.True);
Assert.That(otherWay.IsAmbiguous, Is.True);
Assert.That(oneWay.Selector, Is.SameAs(otherWay.Selector), "Which of the two answers must not depend on the order they arrived in.");
});
}
}
[Test]
@@ -190,11 +190,11 @@ public sealed class ModelFamilyIndexTests
var index = ModelFamilyIndex.Build([Selector("llama", MatchKind.SEGMENT, new() { Adds = Capability.TEXT_INPUT })]);
var resolution = index.Explain(new ModelId("something-nobody-wrote-a-rule-for"), ANY_PROVIDER, ModelVendor.UNKNOWN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(resolution.IsKnown, Is.False);
Assert.That(resolution.Profile, Is.EqualTo(ModelProfile.UNKNOWN));
});
}
}
[Test]
@@ -227,11 +227,11 @@ public sealed class ModelFamilyIndexTests
//
var index = ModelFamilyIndex.Build([Selector("GPT-5", MatchKind.PREFIX, new() { Adds = Capability.TEXT_INPUT })]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(index.Rules.Where(rule => !rule.Pattern.IsWellFormed), Is.Not.Empty);
Assert.That(index.Explain(new ModelId("gpt-5.1"), ANY_PROVIDER, ModelVendor.UNKNOWN).IsKnown, Is.False);
});
}
}
private static ModelRule Selector(string text, MatchKind kind, ModelProfileChange change, string origin = "test") => new(new() { Kind = kind, Text = text }, ModelRuleKind.SELECTOR, change, origin);
+8 -8
View File
@@ -30,11 +30,11 @@ public sealed class ModelIdTests
{
var id = new ModelId(reported);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(id.IsEmpty, Is.True);
Assert.That(id.Normalized, Is.Empty);
});
}
}
[Test]
@@ -42,11 +42,11 @@ public sealed class ModelIdTests
{
var id = new ModelId("Qwen3.8:27B-MLX");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(id.Original, Is.EqualTo("Qwen3.8:27B-MLX"));
Assert.That(id.ToString(), Is.EqualTo("Qwen3.8:27B-MLX"));
});
}
}
[Test]
@@ -54,13 +54,13 @@ public sealed class ModelIdTests
{
ModelId untouched = default;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(untouched.IsEmpty, Is.True);
Assert.That(untouched.Original, Is.Empty);
Assert.That(untouched.Normalized, Is.Empty);
Assert.That(untouched.Segments.GetEnumerator().MoveNext(), Is.False);
});
}
}
[Test]
@@ -69,11 +69,11 @@ public sealed class ModelIdTests
var fromOllama = new ModelId("Qwen3.8:27b");
var fromHub = new ModelId("qwen3.8-27b");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(fromOllama, Is.EqualTo(fromHub));
Assert.That(fromOllama.GetHashCode(), Is.EqualTo(fromHub.GetHashCode()));
});
}
}
[Test]
@@ -82,10 +82,10 @@ public sealed class RuleSpecificityTests
var winner = RuleSpecificity.Of(expectedWinner);
var loser = RuleSpecificity.Of(expectedLoser);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(winner.CompareTo(loser), Is.GreaterThan(0));
Assert.That(loser.CompareTo(winner), Is.LessThan(0), "The comparison has to say the same thing in both directions.");
});
}
}
}
@@ -53,14 +53,14 @@ public sealed class MistralReleasesTests
// for "mistral-medium-3" would otherwise answer for "mistral-medium-3.5" as well and place
// it eleven months too early, before the release which gave it reasoning.
//
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(MistralReleases.Of(new ModelId("mistral-medium-3"), SOME_LATEST_RELEASE), Is.EqualTo(2505));
Assert.That(MistralReleases.Of(new ModelId("mistral-medium-3.5"), SOME_LATEST_RELEASE), Is.EqualTo(2604));
Assert.That(MistralReleases.Of(new ModelId("mistral-medium-3-5"), SOME_LATEST_RELEASE), Is.EqualTo(2604), "Mistral writes the version separator both ways for the same model.");
Assert.That(MistralReleases.Of(new ModelId("mistral-large-4"), SOME_LATEST_RELEASE), Is.EqualTo(2610));
Assert.That(MistralReleases.Of(new ModelId("mistral-large-4-0"), SOME_LATEST_RELEASE), Is.EqualTo(2610), "The model card names it with a minor version, which changes nothing about the release.");
});
}
}
[Test]
@@ -73,14 +73,14 @@ public sealed class MistralReleasesTests
var beforeItCouldSee = ModelRegistry.Shared.Profile(LLMProviders.MISTRAL, "mistral-large-2411");
var afterwards = ModelRegistry.Shared.Profile(LLMProviders.MISTRAL, "mistral-large-2512");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(beforeItCouldSee.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.False);
Assert.That(beforeItCouldSee.Reasoning, Is.EqualTo(ReasoningSupport.NONE));
Assert.That(afterwards.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True);
Assert.That(afterwards.Reasoning, Is.EqualTo(ReasoningSupport.OPTIONAL));
});
}
}
[TestCase("mistral-large-4", TestName = "Mistral Large 4 under the name of Mistral's changelog")]
@@ -93,12 +93,12 @@ public sealed class MistralReleasesTests
//
var profile = ModelRegistry.Shared.Profile(LLMProviders.MISTRAL, modelId);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True);
Assert.That(profile.Reasoning, Is.EqualTo(ReasoningSupport.OPTIONAL));
Assert.That(profile.Context.DefaultTokens, Is.EqualTo(1_000_000));
});
}
}
[TestCase("mistral-large-2512", TestName = "Mistral Large 3 keeps its own window")]
@@ -119,12 +119,12 @@ public sealed class MistralReleasesTests
//
var profile = ModelRegistry.Shared.Profile(LLMProviders.MISTRAL, "mistral-large-whenever");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.FUNCTION_CALLING), Is.True, "What the family could always do is still stated.");
Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.False);
Assert.That(profile.Reasoning, Is.EqualTo(ReasoningSupport.NONE));
});
}
}
[Test]
@@ -132,10 +132,10 @@ public sealed class MistralReleasesTests
{
var newest = ModelRegistry.Shared.Profile(LLMProviders.MISTRAL, "ministral-3b-latest");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(newest.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True, "Ministral 3 reads images.");
Assert.That(newest.Reasoning, Is.EqualTo(ReasoningSupport.NONE), "No Ministral reasons, whatever its release.");
});
}
}
}
+10 -10
View File
@@ -20,11 +20,11 @@ public sealed class ModelFactsTests
{
ContextWindow untouched = default;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(untouched.IsKnown, Is.False);
Assert.That(untouched, Is.EqualTo(ContextWindow.UNKNOWN));
});
}
}
[Test]
@@ -32,12 +32,12 @@ public sealed class ModelFactsTests
{
var window = ContextWindow.Of(128_000, 1_000_000);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(window.IsKnown, Is.True);
Assert.That(window.DefaultTokens, Is.EqualTo(128_000));
Assert.That(window.RaisableToTokens, Is.EqualTo(1_000_000));
});
}
}
[Test]
@@ -59,11 +59,11 @@ public sealed class ModelFactsTests
{
TokenizerRef untouched = default;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(untouched.IsKnown, Is.False);
Assert.That(untouched.Kind, Is.EqualTo(TokenizerKind.UNKNOWN));
});
}
}
[Test]
@@ -79,12 +79,12 @@ public sealed class ModelFactsTests
{
ImageLimits untouched = default;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(untouched.IsKnown, Is.False);
Assert.That(untouched.MaxPerMessage, Is.Null);
Assert.That(untouched.MaxPerRequest, Is.Null);
});
}
}
[Test]
@@ -92,11 +92,11 @@ public sealed class ModelFactsTests
{
var noImages = new ImageLimits(MaxPerMessage: 0, MaxPerRequest: null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(noImages.IsKnown, Is.True, "Zero images is a statement an operator can make.");
Assert.That(noImages.MaxPerMessage, Is.EqualTo(0));
});
}
}
[Test]
+10 -10
View File
@@ -21,11 +21,11 @@ public sealed class ModelFamilyTests
{
var family = new SampleFamily();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(family.Name, Is.EqualTo(nameof(SampleFamily)));
Assert.That(family.Rules.Select(rule => rule.Origin), Is.All.EqualTo(nameof(SampleFamily)));
});
}
}
[Test]
@@ -52,7 +52,7 @@ public sealed class ModelFamilyTests
var index = ModelFamilyIndex.Build(new SampleFamily().Rules);
var codex = index.Resolve(new ModelId("gpt-5.1-codex-max"), LLMProviders.OPEN_AI, ModelVendor.OPEN_AI);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(codex.Has(Capability.WEB_SEARCH), Is.False, "This is the one thing the variant takes away.");
Assert.That(codex.Has(Capability.TEXT_INPUT | Capability.MULTIPLE_IMAGE_INPUT | Capability.FUNCTION_CALLING), Is.True);
@@ -60,7 +60,7 @@ public sealed class ModelFamilyTests
Assert.That(codex.Context.DefaultTokens, Is.EqualTo(400_000));
Assert.That(codex.Tokenizer.Id, Is.EqualTo("o200k_base"));
Assert.That(codex.SystemPromptRole, Is.EqualTo(SystemPromptRole.DEVELOPER));
});
}
}
[Test]
@@ -69,11 +69,11 @@ public sealed class ModelFamilyTests
var index = ModelFamilyIndex.Build(new SampleFamily().Rules.Concat(new FamilyWithAModifier().Rules));
var adjusted = index.Resolve(new ModelId("gpt-5.1-base"), LLMProviders.OPEN_AI, ModelVendor.OPEN_AI);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(adjusted.Has(Capability.FUNCTION_CALLING), Is.False, "The modifier did apply.");
Assert.That(adjusted.SystemPromptRole, Is.EqualTo(SystemPromptRole.DEVELOPER));
});
}
}
[Test]
@@ -99,11 +99,11 @@ public sealed class ModelFamilyTests
{
var index = ModelFamilyIndex.Build(new FamilyWithTwoGenerations().Rules);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(index.Resolve(new ModelId("thing3-mini"), LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN).Reasoning, Is.EqualTo(ReasoningSupport.ALWAYS));
Assert.That(index.Resolve(new ModelId("thing4"), LLMProviders.SELF_HOSTED, ModelVendor.UNKNOWN).Reasoning, Is.EqualTo(ReasoningSupport.NONE));
});
}
}
[Test]
@@ -174,12 +174,12 @@ public sealed class ModelFamilyTests
[Test]
public void ASourceWithoutAPageOrADayIsNotAStatement()
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(new SampleFamily().Source.IsStated, Is.True);
Assert.That(new ModelSource(string.Empty, new DateOnly(2026, 9, 11), "a note").IsStated, Is.False);
Assert.That(new ModelSource("https://example.invalid", default, "a note").IsStated, Is.False);
});
}
}
/// <summary>
+6 -6
View File
@@ -24,7 +24,7 @@ public sealed class ModelKindTests
[Test]
public void EveryExampleIsRecognizedAsWhatItIsMadeFor()
{
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
foreach (var example in ModelKindCorpus.ENTRIES)
{
@@ -32,7 +32,7 @@ public sealed class ModelKindTests
Assert.That(profile.Kind, Is.EqualTo(example.Kind), $"{example.Provider} \"{example.ModelId}\"");
}
});
}
}
[Test]
@@ -59,11 +59,11 @@ public sealed class ModelKindTests
//
var profile = ModelRegistry.Shared.Profile(LLMProviders.SELF_HOSTED, "llama-guard-3-8b");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Kind, Is.EqualTo(ModelKind.MODERATION));
Assert.That(profile.Has(Capability.TEXT_INPUT), Is.True, "The family which chose the model still speaks for it.");
});
}
}
[Test]
@@ -71,10 +71,10 @@ public sealed class ModelKindTests
{
var resolution = ModelRegistry.Shared.Explain(LLMProviders.SELF_HOSTED, "bge-reranker-v2-m3");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(resolution.Profile.Kind, Is.EqualTo(ModelKind.RERANKING));
Assert.That(resolution.Modifiers.Select(modifier => modifier.Pattern.Text), Does.Contain("bge"), "Both words match; the ranked one has to be the one which gets the last word.");
});
}
}
}
+8 -8
View File
@@ -14,7 +14,7 @@ public sealed class ModelProfileTests
{
var untouched = ModelProfile.UNKNOWN;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(untouched.Capabilities, Is.EqualTo(Capability.NONE));
Assert.That(untouched.Reasoning, Is.EqualTo(ReasoningSupport.NONE));
@@ -27,7 +27,7 @@ public sealed class ModelProfileTests
// meaning "no idea".
//
Assert.That(untouched.Kind, Is.EqualTo(ModelKind.CHAT));
});
}
}
[Test]
@@ -35,13 +35,13 @@ public sealed class ModelProfileTests
{
var profile = new ModelProfile { Capabilities = Capability.TEXT_INPUT | Capability.TEXT_OUTPUT };
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.TEXT_INPUT | Capability.TEXT_OUTPUT), Is.True);
Assert.That(profile.Has(Capability.TEXT_INPUT | Capability.WEB_SEARCH), Is.False);
Assert.That(profile.HasAny(Capability.TEXT_INPUT | Capability.WEB_SEARCH), Is.True);
Assert.That(profile.HasAny(Capability.WEB_SEARCH | Capability.EMBEDDING), Is.False);
});
}
}
[Test]
@@ -69,13 +69,13 @@ public sealed class ModelProfileTests
var after = new ModelProfileChange { Removes = Capability.WEB_SEARCH }.ApplyTo(before);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(after.Capabilities, Is.EqualTo(Capability.TEXT_INPUT));
Assert.That(after.Reasoning, Is.EqualTo(ReasoningSupport.OPTIONAL), "A change saying nothing about reasoning must not reset it.");
Assert.That(after.Context, Is.EqualTo(before.Context), "A change saying nothing about the context window must not reset it.");
Assert.That(after.SystemPromptRole, Is.EqualTo(SystemPromptRole.DEVELOPER), "A change saying nothing about the system prompt role must not reset it.");
});
}
}
[Test]
@@ -106,11 +106,11 @@ public sealed class ModelProfileTests
var profile = change.ApplyTo(ModelProfile.UNKNOWN);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Capabilities, Is.EqualTo(Capability.TEXT_INPUT));
Assert.That(profile.Has(Capability.ALWAYS_REASONING), Is.False);
Assert.That(profile.Reasoning, Is.EqualTo(ReasoningSupport.ALWAYS));
});
}
}
}
+2 -2
View File
@@ -27,7 +27,7 @@ public sealed class Gpt6FamilyTests
{
var profile = LLMProviders.OPEN_AI.GetModelProfile(new Model(modelId, null));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.RESPONSES_API), Is.True);
Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True);
@@ -35,7 +35,7 @@ public sealed class Gpt6FamilyTests
Assert.That(profile.Has(Capability.WEB_SEARCH), Is.True);
Assert.That(profile.Context.IsKnown, Is.True);
Assert.That(profile.Context.DefaultTokens, Is.EqualTo(1_050_000));
});
}
}
[TestCase("gpt-6-astra", ReasoningSupport.ALWAYS, TestName = "Astra reasons on every request")]
@@ -27,13 +27,13 @@ public sealed class NewOpenAIModelTests
{
var profile = LLMProviders.OPEN_AI.GetModelProfile(new Model(modelId, null));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.RESPONSES_API), Is.True);
Assert.That(profile.Has(Capability.FUNCTION_CALLING), Is.True);
Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True);
Assert.That(profile.Reasoning, Is.Not.EqualTo(ReasoningSupport.NONE));
});
}
}
[Test]
@@ -46,12 +46,12 @@ public sealed class NewOpenAIModelTests
//
var profile = LLMProviders.OPEN_AI.GetModelProfile(new Model("gpt-oss-120b", null));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.RESPONSES_API), Is.False);
Assert.That(profile.Reasoning, Is.EqualTo(ReasoningSupport.NONE));
Assert.That(profile.Context.DefaultTokens, Is.EqualTo(131_072));
});
}
}
[Test]
@@ -59,11 +59,11 @@ public sealed class NewOpenAIModelTests
{
var profile = LLMProviders.SELF_HOSTED.GetModelProfile(new Model("gpt-j-6b", null));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.RESPONSES_API), Is.False);
Assert.That(profile.Has(Capability.MULTIPLE_IMAGE_INPUT), Is.False);
Assert.That(profile.Reasoning, Is.EqualTo(ReasoningSupport.NONE));
});
}
}
}
@@ -49,11 +49,11 @@ public sealed class DeclaredModelsTests
var profile = registry.Profile(LLMProviders.SELF_HOSTED, "acme-assistant-7b");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(profile.Has(Capability.FUNCTION_CALLING), Is.False, "The built-in rule grants this one, and the declaration does not.");
Assert.That(profile.Reasoning, Is.EqualTo(ReasoningSupport.NONE), "Nor does it reason, whatever the built-in rule says.");
});
}
}
[Test]
@@ -83,11 +83,11 @@ public sealed class DeclaredModelsTests
registry.Declare([]);
var afterwards = registry.Profile(LLMProviders.SELF_HOSTED, "acme-assistant-7b");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(whileDeclared.Has(Capability.FUNCTION_CALLING), Is.False);
Assert.That(afterwards.Has(Capability.FUNCTION_CALLING), Is.True);
});
}
}
[Test]
@@ -105,11 +105,11 @@ public sealed class DeclaredModelsTests
Declaring("acme-assistant-7b", MatchKind.EXACT, Capability.TEXT_INPUT | Capability.TEXT_OUTPUT | Capability.MULTIPLE_IMAGE_INPUT),
]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(registry.Profile(LLMProviders.SELF_HOSTED, "acme-assistant-7b").Has(Capability.MULTIPLE_IMAGE_INPUT), Is.True);
Assert.That(registry.Profile(LLMProviders.SELF_HOSTED, "acme-assistant-3b").Has(Capability.MULTIPLE_IMAGE_INPUT), Is.False);
});
}
}
[Test]
@@ -127,11 +127,11 @@ public sealed class DeclaredModelsTests
{
registry.Declare([declaration]);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(registry.Profile(LLMProviders.OPEN_AI, "gpt-5.1").Has(Capability.VIDEO_INPUT), Is.True);
Assert.That(registry.Profile(LLMProviders.OPEN_ROUTER, "openai/gpt-5.1").Has(Capability.VIDEO_INPUT), Is.True, "The same model, reached through a gateway which wraps the name.");
});
}
}
finally
{
@@ -56,7 +56,7 @@ public sealed class ModelDeclarationTests
var declaration = await ReadAsync(A_COMPLETE_DECLARATION);
Assert.That(declaration, Is.Not.Null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(declaration!.Pattern.Text, Is.EqualTo("acme-assistant"));
Assert.That(declaration.Pattern.Kind, Is.EqualTo(MatchKind.PREFIX));
@@ -68,7 +68,7 @@ public sealed class ModelDeclarationTests
Assert.That(declaration.Change.Images, Is.EqualTo(new ImageLimits(1, 8)));
Assert.That(declaration.Source.CheckedOn, Is.EqualTo(new DateOnly(2026, 9, 12)));
Assert.That(declaration.EnterpriseConfigurationPluginId, Is.EqualTo(PLUGIN_ID));
});
}
}
[Test]
@@ -77,7 +77,7 @@ public sealed class ModelDeclarationTests
var declaration = await ReadAsync(THE_LEAST_A_DECLARATION_CAN_SAY);
Assert.That(declaration, Is.Not.Null);
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(declaration!.Pattern.Kind, Is.EqualTo(MatchKind.SEGMENT), "The kind to reach for by default, here as everywhere else.");
Assert.That(declaration.Change.Reasoning, Is.EqualTo(ReasoningSupport.NONE));
@@ -85,7 +85,7 @@ public sealed class ModelDeclarationTests
Assert.That(declaration.Change.Context, Is.Null);
Assert.That(declaration.Change.Tokenizer, Is.Null);
Assert.That(declaration.Change.Images, Is.Null);
});
}
}
[Test]
@@ -199,11 +199,11 @@ public sealed class ModelDeclarationTests
var theSameNames = await ReadAsync(A_COMPLETE_DECLARATION.Replace("""["CONTEXT_WINDOW"] = 131072,""", """["CONTEXT_WINDOW"] = 65536,""", StringComparison.Ordinal));
var otherNames = await ReadAsync(A_COMPLETE_DECLARATION.Replace("""["MATCH"] = "PREFIX",""", """["MATCH"] = "SEGMENT",""", StringComparison.Ordinal));
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(theSameNames?.Id, Is.EqualTo(declaration?.Id), "The same pattern, so one of the two has to win.");
Assert.That(otherNames?.Id, Is.Not.EqualTo(declaration?.Id), "Bound to the name differently, so they claim different sets of names.");
});
}
}
private static async Task<ModelDeclaration?> ReadAsync(string entry)
+4 -4
View File
@@ -27,7 +27,7 @@ public sealed class PortingDifferenceTests
{
var corrected = ExpectedChanges.ENTRIES.Where(change => IsAnswered(change.Provider, change.ModelId)).ToList();
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(corrected, Is.Not.Empty, "Nothing the audit found wrong is answered by a rule at all, which would make this test green for the wrong reason.");
@@ -38,7 +38,7 @@ public sealed class PortingDifferenceTests
Assert.That(rebuilt, Is.EqualTo(CapabilitySnapshot.Describe(change.AnswerWanted)), $"{change.Provider} \"{change.ModelId}\": {change.Reason}");
}
});
}
}
[Test]
@@ -80,7 +80,7 @@ public sealed class PortingDifferenceTests
// toss. Reading the rules alone cannot find it -- the two patterns are written differently
// and only meet on a real name, which is what the corpus is full of.
//
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
foreach (var entry in ModelCorpus.ENTRIES)
{
@@ -88,7 +88,7 @@ public sealed class PortingDifferenceTests
Assert.That(resolution.IsAmbiguous, Is.False, $"{entry.Provider} \"{entry.ModelId}\" is claimed by {resolution.Selector} and, just as strongly, by {string.Join(", ", resolution.TiedSelectors)}.");
}
});
}
}
/// <summary>
@@ -114,12 +114,12 @@ public sealed class ModelRegistryTests
var atOpenAI = ModelRegistry.Shared.Profile(LLMProviders.OPEN_AI, "gpt-5.1");
var throughAGateway = ModelRegistry.Shared.Profile(LLMProviders.OPEN_ROUTER, "openai/gpt-5.1");
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(atOpenAI.Has(Capability.RESPONSES_API), Is.True);
Assert.That(throughAGateway.Has(Capability.RESPONSES_API), Is.False);
Assert.That(throughAGateway.Has(Capability.FUNCTION_CALLING), Is.True, "Everything but the API survives the trip through a gateway.");
});
}
}
[Test]
+8 -8
View File
@@ -40,11 +40,11 @@ public sealed class TokenizerRuleTests
{
var tokenizer = provider.GetModelProfile(new Model(modelId, null)).Tokenizer;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(tokenizer.Kind, Is.EqualTo(TokenizerKind.TIKTOKEN));
Assert.That(tokenizer.Id, Is.EqualTo(encoding));
});
}
}
[TestCase(LLMProviders.ANTHROPIC, "claude-opus-5", "/v1/messages/count_tokens")]
@@ -55,11 +55,11 @@ public sealed class TokenizerRuleTests
{
var tokenizer = provider.GetModelProfile(new Model(modelId, null)).Tokenizer;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(tokenizer.Kind, Is.EqualTo(TokenizerKind.PROVIDER_API));
Assert.That(tokenizer.Id, Is.EqualTo(endpoint));
});
}
}
[TestCase(LLMProviders.OPEN_AI, "gpt-6-astra", Description = "Newer than the mapping OpenAI publishes, so nothing is claimed for it.")]
@@ -70,11 +70,11 @@ public sealed class TokenizerRuleTests
{
var tokenizer = provider.GetModelProfile(new Model(modelId, null)).Tokenizer;
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(tokenizer.IsKnown, Is.False);
Assert.That(tokenizer.Kind, Is.EqualTo(TokenizerKind.UNKNOWN), "Which means the built-in tokenizer, the same as today.");
});
}
}
[Test]
@@ -98,12 +98,12 @@ public sealed class TokenizerRuleTests
// without a kind cannot be told apart from any other string -- whether it is a repository,
// an encoding or an endpoint decides what a person can do with it.
//
Assert.Multiple(() =>
using (Assert.EnterMultipleScope())
{
Assert.That(new TokenizerRef(TokenizerKind.HUGGING_FACE, string.Empty).IsKnown, Is.False);
Assert.That(new TokenizerRef(TokenizerKind.UNKNOWN, "o200k_base").IsKnown, Is.False);
Assert.That(TokenizerRef.UNKNOWN.IsKnown, Is.False);
Assert.That(new TokenizerRef(TokenizerKind.TIKTOKEN, "o200k_base").IsKnown, Is.True);
});
}
}
}
Loaded 100 of 327 files, more files were not shown because too many files have changed in this diff. Show more