mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 15:27:21 +00:00
Upgraded to .NET 10 (#1147)
This commit is contained in:
1 parent
faae2951f7
commit
fc7399e81b
327 files changed
+3059
-3049
No files matched your search
@@ -74,7 +74,7 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var (original, snippet) = Export(type);
|
||||
var imported = Import(snippet, type).ConfigurationObject;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(Guid.TryParse(imported.Id, out _), Is.True, "Every configuration object needs a GUID as its ID.");
|
||||
Assert.That(imported.Id, Is.Not.EqualTo(original.Id).And.Not.EqualTo(ExportedId(snippet, type)), "Importing the same snippet twice must not create two objects with one ID.");
|
||||
@@ -82,7 +82,7 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
Assert.That(imported.IsEnterpriseConfiguration, Is.False);
|
||||
Assert.That(imported.EnterpriseConfigurationPluginId, Is.EqualTo(Guid.Empty));
|
||||
Assert.That(imported.Name, Is.EqualTo(original.Name));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -108,7 +108,7 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var template = (ChatTemplate)original;
|
||||
var imported = (ChatTemplate)Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE).ConfigurationObject;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(imported.SystemPrompt, Is.EqualTo(template.SystemPrompt));
|
||||
Assert.That(imported.PredefinedUserPrompt, Is.EqualTo(template.PredefinedUserPrompt));
|
||||
@@ -121,7 +121,7 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
Assert.That(imported.ProviderId, Is.EqualTo(template.ProviderId));
|
||||
Assert.That(imported.ProfileId, Is.EqualTo(template.ProfileId));
|
||||
Assert.That(imported.WorkspaceName, Is.EqualTo(template.WorkspaceName));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -131,7 +131,7 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var policy = (DataDocumentAnalysisPolicy)original;
|
||||
var imported = (DataDocumentAnalysisPolicy)Import(snippet, PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY).ConfigurationObject;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(imported.IsProtected, Is.False, "A configuration plugin protects its policies; the user's own policy is the user's to edit.");
|
||||
Assert.That(imported.PolicyDescription, Is.EqualTo(policy.PolicyDescription));
|
||||
@@ -142,7 +142,7 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
Assert.That(imported.PreselectedProvider, Is.EqualTo(policy.PreselectedProvider));
|
||||
Assert.That(imported.PreselectedProfile, Is.EqualTo(policy.PreselectedProfile));
|
||||
Assert.That(imported.HidePolicyDefinition, Is.EqualTo(policy.HidePolicyDefinition));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -150,22 +150,22 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
{
|
||||
const string PROFILE_WITHOUT_ACTIONS = """CONFIG["PROFILES"][#CONFIG["PROFILES"]+1] = { ["Id"] = "6b8e1a2c-3d4f-4a5b-9c6d-7e8f9a0b1c2d", ["Name"] = "Writer", ["NeedToKnow"] = "I write." }""";
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(ConfigurationSnippetImport.TryImport(PROFILE_WITHOUT_ACTIONS, PluginConfigurationObjectType.PROFILE, out var result, out var issue), Is.False);
|
||||
Assert.That(result, Is.Null);
|
||||
Assert.That(issue, Is.Not.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ASnippetWhichCannotBeReadIsRefused()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(ConfigurationSnippetImport.TryImport("CONFIG[\"PROFILES\"] = os.getenv(\"HOME\")", PluginConfigurationObjectType.PROFILE, out _, out var issue), Is.False);
|
||||
Assert.That(issue, Is.Not.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||
@@ -175,11 +175,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
{
|
||||
var result = Import(ExportProvider(type, Encrypt(API_KEY)).Snippet, type);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Secret, Is.EqualTo(API_KEY));
|
||||
Assert.That(result.Hints, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||
@@ -226,11 +226,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
""");
|
||||
|
||||
var imported = (IUserProvidedAPIKey)Import(snippetWithPrompt, type).ConfigurationObject;
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(imported.PromptForUserAPIKey, Is.False);
|
||||
Assert.That(imported.AllowUserProvidedAPIKey, Is.False);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -274,12 +274,12 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var result = Import(snippet, PluginConfigurationObjectType.DATA_SOURCE);
|
||||
var dataSource = (DataSourceERI_V1)result.ConfigurationObject;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(dataSource.Username, Is.EqualTo(expectedUsername));
|
||||
Assert.That(dataSource.UsernamePasswordMode, Is.EqualTo(DataSourceERIUsernamePasswordMode.USER_MANAGED), "Only an organization manages the sign-in for its users.");
|
||||
Assert.That(result.Secret, Is.EqualTo(TOKEN));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(null, OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, TestName = "A data source without a level gets the one of a new data source")]
|
||||
@@ -312,11 +312,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var result = Import(ExportChatTemplate([existing, missing, RELATIVE, WINDOWS_SHARE, UNIX_SHARE]), PluginConfigurationObjectType.CHAT_TEMPLATE);
|
||||
var template = (ChatTemplate)result.ConfigurationObject;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(template.FileAttachments.Select(attachment => attachment.FilePath), Is.EqualTo(new[] { existing }));
|
||||
Assert.That(result.AttachmentsToRelink, Is.EqualTo(new[] { missing, RELATIVE, WINDOWS_SHARE, UNIX_SHARE }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -333,11 +333,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
{
|
||||
var result = Import(ExportChatTemplate([]), PluginConfigurationObjectType.CHAT_TEMPLATE);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Hints, Is.Empty);
|
||||
Assert.That(result.AttachmentsToRelink, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||
@@ -349,11 +349,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var copy = this.CreateTokenizerCopy(type, original.Id);
|
||||
var result = Import(snippet, type);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.EqualTo(copy), "Imported on the computer it came from, the export leads back to the copy of the provider.");
|
||||
Assert.That(result.Hints, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -368,11 +368,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var storedFingerprint = await TokenizerFingerprint.KeepOrReadAsync(imported.TokenizerPath, imported.TokenizerFingerprint);
|
||||
var fingerprintOfTheCopiedFile = await TokenizerFingerprint.ForFileAsync(copy);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(imported.TokenizerFingerprint, Is.Empty, "The import reads no file before the user agreed to it.");
|
||||
Assert.That(storedFingerprint, Is.Not.Empty.And.EqualTo(fingerprintOfTheCopiedFile), "Without the fingerprint, the embedding signature cannot tell the tokenizer apart from no tokenizer at all.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||
@@ -381,11 +381,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
{
|
||||
var result = Import(ExportProvider(type, null, Path.Combine(this.temporaryDirectory, "missing-tokenizer.json")).Snippet, type);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.Empty, "Saving the provider would fail on a tokenizer which cannot be copied.");
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains("missing-tokenizer.json"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||
@@ -395,11 +395,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var tokenizer = this.CreateFile("tokenizer.json");
|
||||
var result = Import(ExportProviderWithTokenizerPath(type, tokenizer), type);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.EqualTo(tokenizer), "Versions before v26.10.1 exported the absolute path of the tokenizer.");
|
||||
Assert.That(result.Hints, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(PluginConfigurationObjectType.LLM_PROVIDER)]
|
||||
@@ -410,11 +410,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
File.WriteAllText(Path.Combine(this.dataDirectory, "settings.json"), "{}");
|
||||
var result = Import(ExportProviderWithTokenizerPath(type, "tokenizers/../settings.json"), type);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(TokenizerPathOf(result.ConfigurationObject), Is.Empty, "Only the folder of the tokenizers holds what an export may name.");
|
||||
Assert.That(result.Hints, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -424,14 +424,14 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var snippet = ExportPolicy(preselectedProvider: "missing-provider", preselectedProfile: missingProfileId, allowedToolIds: [TOOL_ID, "missing_tool"]);
|
||||
var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY), this.SettingsManager, this.CreateRegistry(new TestTool(Definition())));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Hints, Has.Count.EqualTo(3), "One sentence for each kind of reference.");
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains("missing-provider"));
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains(missingProfileId));
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains("missing_tool"));
|
||||
Assert.That(result.Hints, Has.None.Contains(TOOL_ID), "The tool exists.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -443,14 +443,14 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var snippet = ExportChatTemplate([], toolIds: [TOOL_ID, "missing_tool"], dataSourceIds: [missingDataSourceId], providerId: missingProviderId, profileId: missingProfileId);
|
||||
var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, this.CreateRegistry(new TestTool(Definition())));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Hints, Has.Count.EqualTo(4), "One sentence for each kind of reference.");
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains(missingDataSourceId));
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains("missing_tool"));
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains(missingProviderId));
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains(missingProfileId));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -459,11 +459,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var snippet = ExportChatTemplate([], profileId: Profile.NO_PROFILE.Id);
|
||||
var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, this.CreateRegistry(new TestTool(Definition())));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Hints, Is.Empty, "Starting without a profile is a statement, not a reference to a profile which could be missing.");
|
||||
Assert.That(((ChatTemplate)result.ConfigurationObject).ProfileId, Is.EqualTo(Profile.NO_PROFILE.Id));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -478,11 +478,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var snippet = ExportChatTemplate([], providerId: provider.Id);
|
||||
var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, this.CreateRegistry(new TestTool(Definition())));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Hints, Has.Count.EqualTo(1), "The provider exists, but chats started with the template would not use it.");
|
||||
Assert.That(result.Hints, Has.Exactly(1).Contains(provider.InstanceName));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -494,11 +494,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
|
||||
var result = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(snippet, PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, this.CreateRegistry(new TestTool(Definition())));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Hints, Has.Count.EqualTo(1), "Whether the profile exists does not matter, since chats ignore it anyway.");
|
||||
Assert.That(result.Hints, Has.None.Contains(missingProfileId));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -515,11 +515,11 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var policy = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportPolicy(provider.Id, profile.Id, [TOOL_ID]), PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY), this.SettingsManager, registry);
|
||||
var template = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportChatTemplate([], [TOOL_ID], [dataSource.Id], provider.Id, profile.Id), PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, registry);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(policy.Hints, Is.Empty);
|
||||
Assert.That(template.Hints, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -531,13 +531,13 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var policy = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportPolicy(allowedToolIds: [TOOL_ID]), PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY), this.SettingsManager, registry);
|
||||
var template = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportChatTemplate([], [TOOL_ID]), PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, registry);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(((DataDocumentAnalysisPolicy)policy.ConfigurationObject).AllowedToolIds, Is.EquivalentTo(new[] { COLLECTION_ID }));
|
||||
Assert.That(((ChatTemplate)template.ConfigurationObject).ToolIds, Is.EquivalentTo(new[] { COLLECTION_ID }));
|
||||
Assert.That(policy.Hints, Is.Empty);
|
||||
Assert.That(template.Hints, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -549,13 +549,13 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
var policy = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportPolicy(allowedToolIds: [TOOL_ID]), PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY), this.SettingsManager, registry);
|
||||
var template = await ConfigurationSnippetImport.ResolveReferencesAsync(Import(ExportChatTemplate([], [TOOL_ID]), PluginConfigurationObjectType.CHAT_TEMPLATE), this.SettingsManager, registry);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(policy.Hints, Is.Empty, "The tool exists on this computer; switching it on again makes the policy use it.");
|
||||
Assert.That(template.Hints, Is.Empty);
|
||||
Assert.That(((DataDocumentAnalysisPolicy)policy.ConfigurationObject).AllowedToolIds, Is.EquivalentTo(new[] { TOOL_ID }), "The import keeps the tool.");
|
||||
Assert.That(((ChatTemplate)template.ConfigurationObject).ToolIds, Is.EquivalentTo(new[] { TOOL_ID }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -574,21 +574,21 @@ public sealed class ConfigurationSnippetImportTests : ToolRegistryTestBase
|
||||
|
||||
private static void AssertRefused(string snippet, PluginConfigurationObjectType type)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(ConfigurationSnippetImport.TryImport(snippet, type, out var result, out var issue), Is.False);
|
||||
Assert.That(result, Is.Null);
|
||||
Assert.That(issue, Is.Not.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertAPIKeyLeftOut(ConfigurationSnippetImportResult result)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(result.Secret, Is.Empty);
|
||||
Assert.That(result.Hints, Has.Count.EqualTo(1), "The provider is imported, but the user has to enter a key.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -159,11 +159,11 @@ public sealed class ConfigurationSnippetParserTests
|
||||
private static void AssertRefused(string snippet, PluginConfigurationObjectType type = PluginConfigurationObjectType.PROFILE)
|
||||
{
|
||||
var accepted = ConfigurationSnippetParser.TryParse(snippet, type, out _, out var issue);
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(accepted, Is.False);
|
||||
Assert.That(issue, Is.Not.Empty, "A refused snippet needs a reason the user can read.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Reflection;
|
||||
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace AIStudio.Tests.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the release finds the plugins which ship with AI Studio, such as the language plugins.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A debug build reads these plugins from the source tree, while the release copies them out of the
|
||||
/// files embedded into the app. A plugin which is missing there would leave the release without its
|
||||
/// language, and only an error in the log would tell.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class InternalPluginResourcesTests
|
||||
{
|
||||
[Test]
|
||||
public void EveryInternalPluginIsEmbedded([Values] InternalPlugin plugin)
|
||||
{
|
||||
var embeddedPlugins = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Plugins");
|
||||
var mainFile = embeddedPlugins.GetFileInfo($"{plugin.MetaData().ResourcePath}/plugin.lua");
|
||||
|
||||
Assert.That(mainFile.Exists, Is.True, $"The release would not find the plugin {plugin}, since it is not among the embedded files.");
|
||||
Assert.That(mainFile.Length, Is.GreaterThan(0), $"The release would find the plugin {plugin} empty.");
|
||||
}
|
||||
}
|
||||
@@ -62,11 +62,11 @@ public sealed class LeftOverConfigurationObjectsTests
|
||||
{
|
||||
var wasChanged = await PluginConfigurationObjectDescriptors.CHAT_TEMPLATE.CleanLeftOverAsync([new AvailablePlugin()], new HashSet<Guid>(), new HashSet<Guid> { PLUGIN_ID }, []);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(wasChanged, Is.False);
|
||||
Assert.That(this.settingsManager.ConfigurationData.ChatTemplates, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -74,11 +74,11 @@ public sealed class LeftOverConfigurationObjectsTests
|
||||
{
|
||||
var wasChanged = await PluginConfigurationObjectDescriptors.CHAT_TEMPLATE.CleanLeftOverAsync([new AvailablePlugin()], new HashSet<Guid>(), new HashSet<Guid>(), []);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(wasChanged, Is.True, "A plugin which started and no longer defines the template has dropped it, or the test above checks nothing.");
|
||||
Assert.That(this.settingsManager.ConfigurationData.ChatTemplates, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -120,12 +120,12 @@ public sealed class ManagedSettingsCoverageTests
|
||||
.Order(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(registeredSettings.Select(setting => setting.SettingName), Does.Contain("DataApp.UpdateInterval"), "No settings were registered, so this test would check nothing.");
|
||||
Assert.That(plugin.DeclaredSettingsCount, Is.EqualTo(registeredSettings.Count), "The plugin of this test does not declare every registered setting.");
|
||||
Assert.That(unmanagedSettingNames, Is.Empty, "These settings are registered with ManagedConfiguration.Register, but no configuration plugin can manage them. Read each one in PluginConfiguration.TryProcessConfiguration and document it in the example configuration plugin.lua, or add it to EXEMPT_SETTINGS with the reason it must stay out of reach of configuration plugins.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -134,7 +134,7 @@ public sealed class ManagedSettingsCoverageTests
|
||||
using var plugin = await RunPluginWithEveryRegisteredSettingAsync(PLUGIN_ID);
|
||||
var registeredSettings = ManagedConfiguration.RegisteredSettings.ToDictionary(setting => setting.SettingName, StringComparer.Ordinal);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
foreach (var exemptSettingName in EXEMPT_SETTINGS.Keys)
|
||||
{
|
||||
@@ -142,7 +142,7 @@ public sealed class ManagedSettingsCoverageTests
|
||||
if (exemptSetting is not null)
|
||||
Assert.That(IsManagedByThePlugin(exemptSetting), Is.False, $"A configuration plugin can manage the exempt setting '{exemptSettingName}' by now. Remove it from EXEMPT_SETTINGS.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -169,11 +169,11 @@ public sealed class ManagedSettingsCoverageTests
|
||||
.Order(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(settingsOfTheOrganization, Is.Not.Empty, "The organization manages no setting, so this test would check nothing.");
|
||||
Assert.That(takenOverSettingNames, Is.Empty, "A local configuration plugin took these settings over from the organization. Every way of reading a setting from a configuration plugin has to ask ManagedConfiguration.MayManageSetting before it changes anything.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -71,12 +71,12 @@ public sealed class ManagedTokenizerStateTests
|
||||
var configObjects = await StartPluginAsync();
|
||||
var embeddingProvider = this.settingsManager.ConfigurationData.EmbeddingProviders.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(embeddingProvider.TokenizerPath, Is.EqualTo(STORED_EMBEDDING_TOKENIZER_PATH), "Until the tokenizers are synchronized, the stored copy is the tokenizer this provider uses.");
|
||||
Assert.That(embeddingProvider.TokenizerFingerprint, Is.EqualTo(STORED_FINGERPRINT), "An empty fingerprint looks like another tokenizer and costs every data source of this provider its index.");
|
||||
Assert.That(configObjects.Single(x => x.Type == PluginConfigurationObjectType.EMBEDDING_PROVIDER).ConfiguredTokenizerPath, Is.EqualTo(CONFIGURED_TOKENIZER_PATH), "The synchronization learns from here which tokenizer the plugin wants.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -100,11 +100,11 @@ public sealed class ManagedTokenizerStateTests
|
||||
|
||||
var configObjects = await StartPluginAsync();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(this.settingsManager.ConfigurationData.Providers.Single().TokenizerPath, Is.EqualTo(STORED_CHAT_TOKENIZER_PATH), "The path inside the plugin is relative to the plugin, so no token could be counted with it.");
|
||||
Assert.That(configObjects.Single(x => x.Type == PluginConfigurationObjectType.LLM_PROVIDER).ConfiguredTokenizerPath, Is.EqualTo(CONFIGURED_TOKENIZER_PATH), "The synchronization learns from here which tokenizer the plugin wants.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -113,14 +113,14 @@ public sealed class ManagedTokenizerStateTests
|
||||
var configObjects = await StartPluginAsync();
|
||||
var embeddingProvider = this.settingsManager.ConfigurationData.EmbeddingProviders.Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(this.settingsManager.ConfigurationData.Providers.Single().TokenizerPath, Is.Empty, "Nothing is stored yet, and the path inside the plugin is no tokenizer the app could load.");
|
||||
Assert.That(embeddingProvider.TokenizerPath, Is.Empty);
|
||||
Assert.That(embeddingProvider.TokenizerFingerprint, Is.Empty);
|
||||
Assert.That(configObjects, Has.Count.EqualTo(2));
|
||||
Assert.That(configObjects.Select(x => x.ConfiguredTokenizerPath), Is.All.EqualTo(CONFIGURED_TOKENIZER_PATH), "The synchronization still learns which tokenizer to store.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -19,11 +19,11 @@ public sealed class PluginConfigurationObjectDescriptorsTests
|
||||
var describedTypes = PluginConfigurationObjectDescriptors.ALL.Select(descriptor => descriptor.Type).ToList();
|
||||
var configurableTypes = Enum.GetValues<PluginConfigurationObjectType>().Where(type => type is not PluginConfigurationObjectType.NONE and not PluginConfigurationObjectType.UNKNOWN);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(describedTypes, Is.EquivalentTo(configurableTypes));
|
||||
Assert.That(describedTypes, Is.Unique, "A type described twice would be read and cleaned up twice.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -31,17 +31,17 @@ public sealed class PluginConfigurationObjectDescriptorsTests
|
||||
{
|
||||
var tableNames = PluginConfigurationObjectDescriptors.ALL.Select(descriptor => descriptor.LuaTableName).ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(tableNames, Has.None.Empty);
|
||||
Assert.That(tableNames, Is.Unique, "Two types reading the same table would each take the objects of the other.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryDescriptorIsFoundByItsTypeAndByItsTable()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
foreach (var descriptor in PluginConfigurationObjectDescriptors.ALL)
|
||||
{
|
||||
@@ -49,7 +49,7 @@ public sealed class PluginConfigurationObjectDescriptorsTests
|
||||
Assert.That(PluginConfigurationObjectDescriptors.TryGet(descriptor.LuaTableName, out var found), Is.True, descriptor.LuaTableName);
|
||||
Assert.That(found, Is.SameAs(descriptor), descriptor.LuaTableName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(PluginConfigurationObjectType.NONE)]
|
||||
|
||||
@@ -27,12 +27,12 @@ public sealed class PluginLastChangedTests
|
||||
{
|
||||
using var plugin = await LoadModelPlugin(LAST_CHANGED_LINE);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(plugin.IsValid, Is.True, string.Join(" ", plugin.Issues));
|
||||
Assert.That(plugin.LastChanged, Is.EqualTo(LAST_CHANGED));
|
||||
Assert.That(plugin.LastChanged?.Offset, Is.EqualTo(TimeSpan.Zero), "The UI converts from UTC, so the value has to be in UTC.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -40,11 +40,11 @@ public sealed class PluginLastChangedTests
|
||||
{
|
||||
using var plugin = await LoadModelPlugin(string.Empty);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(plugin.IsValid, Is.True, string.Join(" ", plugin.Issues));
|
||||
Assert.That(plugin.LastChanged, Is.Null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("\"2026-10-12\"")]
|
||||
@@ -57,11 +57,11 @@ public sealed class PluginLastChangedTests
|
||||
{
|
||||
using var plugin = await LoadModelPlugin($"LAST_CHANGED = {luaValue}");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(plugin.IsValid, Is.True, "A malformed LAST_CHANGED is no reason to refuse an otherwise valid plugin.");
|
||||
Assert.That(plugin.LastChanged, Is.Null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -86,12 +86,12 @@ public sealed class PluginLastChangedTests
|
||||
var lua = DirectChatLauncherLuaWriter.Write(CreateLauncherMetadata(LAST_CHANGED), CreateLauncherDefinition());
|
||||
using var plugin = await PluginFactory.Load(null, lua);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(lua, Does.Contain(LAST_CHANGED_LINE));
|
||||
Assert.That(plugin.IsValid, Is.True, string.Join(" ", plugin.Issues));
|
||||
Assert.That(plugin.LastChanged, Is.EqualTo(LAST_CHANGED));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -41,11 +41,11 @@ public sealed class PluginLinksTests
|
||||
[Test]
|
||||
public void OnlyAMailtoLinkWritesAMail()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(PluginLinks.TryParse("mailto:support@example.org", out var mail) && PluginLinks.IsMail(mail), Is.True);
|
||||
Assert.That(PluginLinks.TryParse("https://example.org", out var website) && PluginLinks.IsMail(website), Is.False);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(AssistantPluginGenerationService.DEFAULT_SUPPORT_CONTACT, true)]
|
||||
@@ -64,10 +64,10 @@ public sealed class PluginLinksTests
|
||||
[TestCase("https://example.org/plugins?id=1", "https://example.org/plugins?id=1")]
|
||||
public void AMailLinkShowsItsAddressAndAWebsiteShowsItself(string text, string displayText)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(PluginLinks.TryParse(text, out var link), Is.True);
|
||||
Assert.That(PluginLinks.GetDisplayText(link!), Is.EqualTo(displayText));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,13 +63,13 @@ public sealed class TokenizerSyncTests
|
||||
var (storedPath, fingerprint) = await this.SyncAsync(storage, string.Empty);
|
||||
var fingerprintOfTheCopy = await TokenizerFingerprint.ForFileAsync(storedPath);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"validate {this.sourcePath}", $"store {MODEL_ID} {this.sourcePath}" }), "Without a stored copy, there is nothing to compare with.");
|
||||
Assert.That(storedPath, Is.Not.Empty.And.Not.EqualTo(this.sourcePath), "The provider points to the copy, not to the file inside the plugin.");
|
||||
Assert.That(File.ReadAllBytes(storedPath), Is.EqualTo(File.ReadAllBytes(this.sourcePath)));
|
||||
Assert.That(fingerprint, Is.Not.Empty.And.EqualTo(fingerprintOfTheCopy));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -81,12 +81,12 @@ public sealed class TokenizerSyncTests
|
||||
var secondStart = await this.SyncAsync(storage, firstStart.Path);
|
||||
var fingerprintOfTheCopy = await TokenizerFingerprint.ForFileAsync(firstStart.Path);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.Empty, "Checking the same file again would cost most of a second and change nothing.");
|
||||
Assert.That(secondStart, Is.EqualTo(firstStart), "A fingerprint which differs from the one before would cost every data source of the provider its index.");
|
||||
Assert.That(secondStart.Fingerprint, Is.EqualTo(fingerprintOfTheCopy));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -98,12 +98,12 @@ public sealed class TokenizerSyncTests
|
||||
|
||||
var secondStart = await this.SyncAsync(storage, firstStart.Path);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"validate {this.sourcePath}", $"store {MODEL_ID} {this.sourcePath}" }));
|
||||
Assert.That(File.Exists(secondStart.Path), Is.True);
|
||||
Assert.That(secondStart.Fingerprint, Is.EqualTo(firstStart.Fingerprint), "It is the same tokenizer, so the index stays.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -117,12 +117,12 @@ public sealed class TokenizerSyncTests
|
||||
|
||||
var secondStart = await this.SyncAsync(storage, firstStart.Path);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"validate {this.sourcePath}", $"store {MODEL_ID} {this.sourcePath}" }), "Size and time do not tell a damaged copy apart, the content does.");
|
||||
Assert.That(File.ReadAllBytes(secondStart.Path), Is.EqualTo(File.ReadAllBytes(this.sourcePath)));
|
||||
Assert.That(secondStart.Fingerprint, Is.EqualTo(firstStart.Fingerprint));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -135,12 +135,12 @@ public sealed class TokenizerSyncTests
|
||||
var secondStart = await this.SyncAsync(storage, firstStart.Path);
|
||||
var fingerprintOfThePluginFile = await TokenizerFingerprint.ForFileAsync(this.sourcePath);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"validate {this.sourcePath}", $"store {MODEL_ID} {this.sourcePath}" }));
|
||||
Assert.That(secondStart.Fingerprint, Is.Not.EqualTo(firstStart.Fingerprint), "Another tokenizer cuts the text at other places, so the index has to go.");
|
||||
Assert.That(secondStart.Fingerprint, Is.EqualTo(fingerprintOfThePluginFile));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -153,12 +153,12 @@ public sealed class TokenizerSyncTests
|
||||
|
||||
var secondStart = await this.SyncAsync(storage, firstStart.Path);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"validate {this.sourcePath}", $"delete {MODEL_ID}" }), "Neither file can be read, so nothing says they hold the same tokenizer.");
|
||||
Assert.That(secondStart.Path, Is.Empty);
|
||||
Assert.That(secondStart.Fingerprint, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -169,12 +169,12 @@ public sealed class TokenizerSyncTests
|
||||
var (storedPath, fingerprint) = await this.SyncAsync(storage, string.Empty);
|
||||
var fingerprintOfThePluginFile = await TokenizerFingerprint.ForFileAsync(this.sourcePath);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storedPath, Is.Not.Empty);
|
||||
Assert.That(File.Exists(storedPath), Is.False, "Otherwise this test checks nothing.");
|
||||
Assert.That(fingerprint, Is.Not.Empty.And.EqualTo(fingerprintOfThePluginFile), "The runtime copies the file as it is, so the file in the plugin tells what the copy holds.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -184,12 +184,12 @@ public sealed class TokenizerSyncTests
|
||||
|
||||
var (storedPath, fingerprint) = await this.SyncAsync(storage, string.Empty);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"validate {this.sourcePath}", $"delete {MODEL_ID}" }), "A tokenizer stored earlier must not outlive the plugin naming an unusable one.");
|
||||
Assert.That(storedPath, Is.Empty);
|
||||
Assert.That(fingerprint, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -200,13 +200,13 @@ public sealed class TokenizerSyncTests
|
||||
|
||||
var (storedPath, fingerprint) = await this.SyncAsync(storage, firstStart.Path, string.Empty);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"delete {MODEL_ID}" }), "The plugin no longer names a tokenizer, so the one stored for it goes.");
|
||||
Assert.That(File.Exists(firstStart.Path), Is.False);
|
||||
Assert.That(storedPath, Is.Empty);
|
||||
Assert.That(fingerprint, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -217,12 +217,12 @@ public sealed class TokenizerSyncTests
|
||||
|
||||
var (storedPath, fingerprint) = await this.SyncAsync(storage, firstStart.Path, "../outside/tokenizer.json");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"delete {MODEL_ID}" }), "A plugin must not make the app read files from anywhere else on the machine.");
|
||||
Assert.That(storedPath, Is.Empty);
|
||||
Assert.That(fingerprint, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -232,12 +232,12 @@ public sealed class TokenizerSyncTests
|
||||
|
||||
var (storedPath, fingerprint) = await this.SyncAsync(storage, string.Empty);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(storage.Calls, Is.EqualTo(new[] { $"validate {this.sourcePath}", $"store {MODEL_ID} {this.sourcePath}" }));
|
||||
Assert.That(storedPath, Is.Empty);
|
||||
Assert.That(fingerprint, Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in new issue
Block a user