mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 09:27:21 +00:00
Fixed local configuration plugins overriding the tool settings of an organization (#1139)
This commit is contained in:
1 parent
eef475581c
commit
2c2ad0dfde
5 files changed
+579
-80
No files matched your search
@@ -0,0 +1,493 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Lua;
|
||||
using Lua.Standard;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Checks who owns a managed setting when configuration plugins of an organization and local ones
|
||||
/// declare it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A local configuration plugin must never take over a setting an organization manages, while two
|
||||
/// configuration plugins of the same organization may override each other. Whether a plugin speaks for
|
||||
/// an organization depends on the directory it lives in, which only a running app has, so this fixture
|
||||
/// tells ManagedConfiguration which of its plugin IDs belong to an organization. The settings are reached
|
||||
/// through Program.SERVICE_PROVIDER, and the managed state of each setting is static, which is why this
|
||||
/// fixture does not run alongside others.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public sealed class ManagedSettingOwnershipTests
|
||||
{
|
||||
private static readonly Guid ORGANIZATION_PLUGIN_ID = Guid.Parse("5d0e7c31-8a4f-4b62-9e17-2c6b8f4a1d93");
|
||||
private static readonly Guid SECOND_ORGANIZATION_PLUGIN_ID = Guid.Parse("a7c94e02-3b5d-4f81-8d26-7e1f0b9c4a58");
|
||||
private static readonly Guid LOCAL_PLUGIN_ID = Guid.Parse("e2b61f8d-4c07-4a9e-b3d5-90f8a6c2e714");
|
||||
|
||||
private const string PROHIBITED_PROVIDERS = "DataApp.ProhibitedProviders";
|
||||
private const string LOCKED_TOOL_SETTINGS = "DataTools.LockedToolSettings";
|
||||
private const string DEFAULT_TOOL_SETTINGS = "DataTools.DefaultToolSettings";
|
||||
private const string MINIMUM_PROVIDER_CONFIDENCE = "DataTools.MinimumProviderConfidenceByToolId";
|
||||
|
||||
private SettingsManager settingsManager = null!;
|
||||
private ServiceProvider serviceProvider = null!;
|
||||
private IServiceProvider previousServiceProvider = null!;
|
||||
private Func<Guid, bool> previousIsOrganizationPlugin = null!;
|
||||
private ILoggerFactory previousLoggerFactory = null!;
|
||||
|
||||
[SetUp]
|
||||
public void CreateSettings()
|
||||
{
|
||||
// Creating the settings data registers every managed setting:
|
||||
this.settingsManager = new SettingsManager(NullLogger<SettingsManager>.Instance, null!);
|
||||
|
||||
this.previousServiceProvider = Program.SERVICE_PROVIDER;
|
||||
this.serviceProvider = new ServiceCollection().AddSingleton(this.settingsManager).BuildServiceProvider();
|
||||
Program.SERVICE_PROVIDER = this.serviceProvider;
|
||||
|
||||
this.previousIsOrganizationPlugin = ManagedConfiguration.IsOrganizationPlugin;
|
||||
ManagedConfiguration.IsOrganizationPlugin = pluginId => pluginId == ORGANIZATION_PLUGIN_ID || pluginId == SECOND_ORGANIZATION_PLUGIN_ID;
|
||||
|
||||
this.previousLoggerFactory = Program.LOGGER_FACTORY;
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void RestoreApplicationState()
|
||||
{
|
||||
// The managed state of a setting outlives this fixture, so we release what the plugins took over:
|
||||
ManagedConfiguration.CleanupLeftOverManagedConfigurations([], new HashSet<Guid>());
|
||||
ManagedConfiguration.IsOrganizationPlugin = this.previousIsOrganizationPlugin;
|
||||
Program.LOGGER_FACTORY = this.previousLoggerFactory;
|
||||
|
||||
Program.SERVICE_PROVIDER = this.previousServiceProvider;
|
||||
this.serviceProvider.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ALocalPluginCannotTakeOverALockedSettingOfTheOrganization()
|
||||
{
|
||||
await ProcessProhibitedProvidersAsync(ORGANIZATION_PLUGIN_ID, """{ "DEEP_SEEK" }""");
|
||||
var processed = await ProcessProhibitedProvidersAsync(LOCAL_PLUGIN_ID, """{ "MISTRAL" }""");
|
||||
|
||||
ManagedConfiguration.TryGet(x => x.App, x => x.ProhibitedProviders, out var configMeta);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.False, "The setting of the organization is out of reach of a local plugin.");
|
||||
Assert.That(this.settingsManager.ConfigurationData.App.ProhibitedProviders, Is.EquivalentTo(new[] { LLMProviders.DEEP_SEEK }), "The value of the organization has to stay.");
|
||||
Assert.That(configMeta.LockedByConfigPluginId, Is.EqualTo(ORGANIZATION_PLUGIN_ID), "The organization has to stay the owner of the setting.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ALocalPluginCannotTakeOverAnEditableDefaultOfTheOrganization()
|
||||
{
|
||||
await ProcessUpdateIntervalAsync(ORGANIZATION_PLUGIN_ID, "WEEKLY", allowUserOverride: true);
|
||||
var processed = await ProcessUpdateIntervalAsync(LOCAL_PLUGIN_ID, "DAILY", allowUserOverride: false);
|
||||
|
||||
ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var configMeta);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.False, "The editable default of the organization is out of reach of a local plugin.");
|
||||
Assert.That(this.settingsManager.ConfigurationData.App.UpdateInterval, Is.EqualTo(UpdateInterval.WEEKLY), "The value of the organization has to stay.");
|
||||
Assert.That(configMeta.IsLocked, Is.False, "A local plugin must not lock what the organization left to the user.");
|
||||
Assert.That(ManagedConfiguration.GetSettingOwner(configMeta), Is.EqualTo(ORGANIZATION_PLUGIN_ID), "The organization has to stay the owner of the setting.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AnotherOrganizationPluginMayTakeOverTheSetting()
|
||||
{
|
||||
await ProcessProhibitedProvidersAsync(ORGANIZATION_PLUGIN_ID, """{ "DEEP_SEEK" }""");
|
||||
var processed = await ProcessProhibitedProvidersAsync(SECOND_ORGANIZATION_PLUGIN_ID, """{ "MISTRAL" }""");
|
||||
|
||||
ManagedConfiguration.TryGet(x => x.App, x => x.ProhibitedProviders, out var configMeta);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.True, "Two configuration plugins of the same organization may override each other.");
|
||||
Assert.That(this.settingsManager.ConfigurationData.App.ProhibitedProviders, Is.EquivalentTo(new[] { LLMProviders.MISTRAL }), "The plugin applied later wins.");
|
||||
Assert.That(configMeta.LockedByConfigPluginId, Is.EqualTo(SECOND_ORGANIZATION_PLUGIN_ID));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ALocalPluginManagesASettingNoOrganizationManages()
|
||||
{
|
||||
var processed = await ProcessProhibitedProvidersAsync(LOCAL_PLUGIN_ID, """{ "MISTRAL" }""");
|
||||
|
||||
ManagedConfiguration.TryGet(x => x.App, x => x.ProhibitedProviders, out var configMeta);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.True, "Without an organization, a local plugin may manage the setting.");
|
||||
Assert.That(this.settingsManager.ConfigurationData.App.ProhibitedProviders, Is.EquivalentTo(new[] { LLMProviders.MISTRAL }));
|
||||
Assert.That(configMeta.LockedByConfigPluginId, Is.EqualTo(LOCAL_PLUGIN_ID));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(LOCKED_TOOL_SETTINGS)]
|
||||
[TestCase(DEFAULT_TOOL_SETTINGS)]
|
||||
[TestCase(MINIMUM_PROVIDER_CONFIDENCE)]
|
||||
public async Task ALocalPluginCannotReplaceAToolTableOfTheOrganization(string settingName)
|
||||
{
|
||||
var entry = SampleEntryOf(settingName);
|
||||
await ProcessToolTableAsync(settingName, ORGANIZATION_PLUGIN_ID, ToLuaTable(entry.Key, entry.OrganizationValue), allowUserOverride: false);
|
||||
var processed = await ProcessToolTableAsync(settingName, LOCAL_PLUGIN_ID, ToLuaTable(entry.Key, entry.LocalValue), allowUserOverride: false);
|
||||
|
||||
var configMeta = GetToolTableMeta(settingName);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.False, "The table of the organization is out of reach of a local plugin.");
|
||||
Assert.That(this.ToolTableOf(settingName), Is.EquivalentTo(new Dictionary<string, string> { [entry.Key] = entry.OrganizationValue }), "The values of the organization have to stay.");
|
||||
Assert.That(configMeta.LockedByConfigPluginId, Is.EqualTo(ORGANIZATION_PLUGIN_ID), "The organization has to stay the owner of the table.");
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(LOCKED_TOOL_SETTINGS)]
|
||||
[TestCase(DEFAULT_TOOL_SETTINGS)]
|
||||
[TestCase(MINIMUM_PROVIDER_CONFIDENCE)]
|
||||
public async Task ALocalPluginCannotTurnALockedToolTableIntoAnEditableDefault(string settingName)
|
||||
{
|
||||
var entry = SampleEntryOf(settingName);
|
||||
await ProcessToolTableAsync(settingName, ORGANIZATION_PLUGIN_ID, ToLuaTable(entry.Key, entry.OrganizationValue), allowUserOverride: false);
|
||||
var processed = await ProcessToolTableAsync(settingName, LOCAL_PLUGIN_ID, ToLuaTable(entry.Key, entry.LocalValue), allowUserOverride: true);
|
||||
|
||||
var configMeta = GetToolTableMeta(settingName);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.False);
|
||||
Assert.That(configMeta.IsLocked, Is.True, "A local plugin must not hand the user a table the organization locked.");
|
||||
Assert.That(configMeta.LockedByConfigPluginId, Is.EqualTo(ORGANIZATION_PLUGIN_ID));
|
||||
Assert.That(this.ToolTableOf(settingName), Is.EquivalentTo(new Dictionary<string, string> { [entry.Key] = entry.OrganizationValue }));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(LOCKED_TOOL_SETTINGS)]
|
||||
[TestCase(DEFAULT_TOOL_SETTINGS)]
|
||||
[TestCase(MINIMUM_PROVIDER_CONFIDENCE)]
|
||||
public async Task ALocalPluginCannotReplaceAnEditableDefaultToolTableOfTheOrganization(string settingName)
|
||||
{
|
||||
var entry = SampleEntryOf(settingName);
|
||||
await ProcessToolTableAsync(settingName, ORGANIZATION_PLUGIN_ID, ToLuaTable(entry.Key, entry.OrganizationValue), allowUserOverride: true);
|
||||
var processed = await ProcessToolTableAsync(settingName, LOCAL_PLUGIN_ID, ToLuaTable(entry.Key, entry.LocalValue), allowUserOverride: false);
|
||||
|
||||
var configMeta = GetToolTableMeta(settingName);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.False);
|
||||
Assert.That(configMeta.IsLocked, Is.False, "A local plugin must not lock what the organization left to the user.");
|
||||
Assert.That(ManagedConfiguration.GetSettingOwner(configMeta), Is.EqualTo(ORGANIZATION_PLUGIN_ID), "The organization has to stay the owner of the table.");
|
||||
Assert.That(this.ToolTableOf(settingName), Is.EquivalentTo(new Dictionary<string, string> { [entry.Key] = entry.OrganizationValue }));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(LOCKED_TOOL_SETTINGS)]
|
||||
[TestCase(DEFAULT_TOOL_SETTINGS)]
|
||||
[TestCase(MINIMUM_PROVIDER_CONFIDENCE)]
|
||||
public async Task AnotherOrganizationPluginMayReplaceAToolTable(string settingName)
|
||||
{
|
||||
var entry = SampleEntryOf(settingName);
|
||||
await ProcessToolTableAsync(settingName, ORGANIZATION_PLUGIN_ID, ToLuaTable(entry.Key, entry.OrganizationValue), allowUserOverride: false);
|
||||
var processed = await ProcessToolTableAsync(settingName, SECOND_ORGANIZATION_PLUGIN_ID, ToLuaTable(entry.Key, entry.LocalValue), allowUserOverride: false);
|
||||
|
||||
var configMeta = GetToolTableMeta(settingName);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.True, "Two configuration plugins of the same organization may override each other.");
|
||||
Assert.That(this.ToolTableOf(settingName), Is.EquivalentTo(new Dictionary<string, string> { [entry.Key] = entry.LocalValue }), "The plugin applied later wins.");
|
||||
Assert.That(configMeta.LockedByConfigPluginId, Is.EqualTo(SECOND_ORGANIZATION_PLUGIN_ID));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(LOCKED_TOOL_SETTINGS)]
|
||||
[TestCase(DEFAULT_TOOL_SETTINGS)]
|
||||
[TestCase(MINIMUM_PROVIDER_CONFIDENCE)]
|
||||
public async Task ALocalPluginManagesAToolTableNoOrganizationManages(string settingName)
|
||||
{
|
||||
var entry = SampleEntryOf(settingName);
|
||||
var processed = await ProcessToolTableAsync(settingName, LOCAL_PLUGIN_ID, ToLuaTable(entry.Key, entry.LocalValue), allowUserOverride: false);
|
||||
|
||||
var configMeta = GetToolTableMeta(settingName);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.True, "Without an organization, a local plugin may manage the table.");
|
||||
Assert.That(this.ToolTableOf(settingName), Is.EquivalentTo(new Dictionary<string, string> { [entry.Key] = entry.LocalValue }));
|
||||
Assert.That(configMeta.LockedByConfigPluginId, Is.EqualTo(LOCAL_PLUGIN_ID));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the user gets their own minimum confidence levels back once the organization's
|
||||
/// configuration plugin is gone.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only this one of the tool tables holds values of the user: the tool settings of the user live
|
||||
/// elsewhere, and the two other tables belong to configuration plugins alone.
|
||||
/// </remarks>
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public async Task WithdrawingTheOrganizationHandsTheUsersConfidenceLevelsBack(bool allowUserOverride)
|
||||
{
|
||||
this.SetUsersConfidenceLevels(new() { ["read_web_page"] = "MEDIUM" });
|
||||
await ProcessToolTableAsync(MINIMUM_PROVIDER_CONFIDENCE, ORGANIZATION_PLUGIN_ID, ToLuaTable("read_web_page", "HIGH"), allowUserOverride);
|
||||
Assert.That(this.ToolTableOf(MINIMUM_PROVIDER_CONFIDENCE), Is.EquivalentTo(new Dictionary<string, string> { ["read_web_page"] = "HIGH" }), "While the organization manages the table, its level applies.");
|
||||
|
||||
ManagedConfiguration.CleanupLeftOverManagedConfigurations([], new HashSet<Guid>());
|
||||
|
||||
Assert.That(this.ToolTableOf(MINIMUM_PROVIDER_CONFIDENCE), Is.EquivalentTo(new Dictionary<string, string> { ["read_web_page"] = "MEDIUM" }), "Without its plugin, the user has to get their own level back instead of the defaults.");
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public async Task DroppingTheTableFromTheConfigurationHandsTheUsersConfidenceLevelsBack(bool allowUserOverride)
|
||||
{
|
||||
this.SetUsersConfidenceLevels(new() { ["read_web_page"] = "MEDIUM" });
|
||||
await ProcessToolTableAsync(MINIMUM_PROVIDER_CONFIDENCE, ORGANIZATION_PLUGIN_ID, ToLuaTable("read_web_page", "HIGH"), allowUserOverride);
|
||||
|
||||
// The plugin stays, but its next version no longer declares the table:
|
||||
await ProcessToolTableAsync(MINIMUM_PROVIDER_CONFIDENCE, ORGANIZATION_PLUGIN_ID, "nil", allowUserOverride);
|
||||
|
||||
Assert.That(this.ToolTableOf(MINIMUM_PROVIDER_CONFIDENCE), Is.EquivalentTo(new Dictionary<string, string> { ["read_web_page"] = "MEDIUM" }), "Once no plugin manages the table, the user has to get their own level back.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AnEditableDefaultTheUserChangedOutlivesTheOrganization()
|
||||
{
|
||||
this.SetUsersConfidenceLevels(new() { ["read_web_page"] = "MEDIUM" });
|
||||
await ProcessToolTableAsync(MINIMUM_PROVIDER_CONFIDENCE, ORGANIZATION_PLUGIN_ID, ToLuaTable("read_web_page", "HIGH"), allowUserOverride: true);
|
||||
this.SetUsersConfidenceLevels(new() { ["read_web_page"] = "LOW" });
|
||||
|
||||
ManagedConfiguration.CleanupLeftOverManagedConfigurations([], new HashSet<Guid>());
|
||||
|
||||
Assert.That(this.ToolTableOf(MINIMUM_PROVIDER_CONFIDENCE), Is.EquivalentTo(new Dictionary<string, string> { ["read_web_page"] = "LOW" }), "The user decided against the default of the organization, and their decision outlives it.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AnUpdatedEditableDefaultReachesUsersWhoKeptTheOldOne()
|
||||
{
|
||||
await ProcessToolTableAsync(MINIMUM_PROVIDER_CONFIDENCE, ORGANIZATION_PLUGIN_ID, ToLuaTable("read_web_page", "HIGH"), allowUserOverride: true);
|
||||
await ProcessToolTableAsync(MINIMUM_PROVIDER_CONFIDENCE, ORGANIZATION_PLUGIN_ID, ToLuaTable("read_web_page", "MODERATE"), allowUserOverride: true);
|
||||
|
||||
Assert.That(this.ToolTableOf(MINIMUM_PROVIDER_CONFIDENCE), Is.EquivalentTo(new Dictionary<string, string> { ["read_web_page"] = "MODERATE" }), "Whoever kept the previous default gets the new one.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the form in which the last applied editable default of a tool table is stored.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The settings of installed versions hold this form. Another one would make every stored default
|
||||
/// look as if the user had changed it, so that the organization could never update it again.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public async Task TheStoredEditableDefaultOfAToolTableKeepsItsForm()
|
||||
{
|
||||
await ProcessToolTableAsync(MINIMUM_PROVIDER_CONFIDENCE, ORGANIZATION_PLUGIN_ID, """{ ["read_web_page"] = "HIGH", ["mailboxes"] = "MEDIUM" }""", allowUserOverride: true);
|
||||
|
||||
var editableDefaultState = this.settingsManager.ConfigurationData.ManagedEditableDefaults[MINIMUM_PROVIDER_CONFIDENCE];
|
||||
Assert.That(editableDefaultState.LastAppliedValue, Is.EqualTo("mailboxes=MEDIUM\nread_web_page=HIGH"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks that the log names a local configuration plugin only for a setting of the organization
|
||||
/// which the plugin actually declares.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Administrators read this warning as an attempt to take over what their organization configured.
|
||||
/// A local plugin which merely exists, without declaring the setting, attempts nothing, so warning
|
||||
/// about it would bury the real attempts under false ones: one for every setting of the organization,
|
||||
/// every time the plugins load. Both ways of handling a setting are covered, the one for sets and the
|
||||
/// one for single values and tables.
|
||||
/// </remarks>
|
||||
[TestCase(PROHIBITED_PROVIDERS, """{ "DEEP_SEEK" }""", """{ "MISTRAL" }""")]
|
||||
[TestCase(MINIMUM_PROVIDER_CONFIDENCE, """{ ["read_web_page"] = "HIGH" }""", """{ ["read_web_page"] = "NONE" }""")]
|
||||
public async Task ALocalPluginIsReportedOnlyForSettingsItDeclares(string settingName, string organizationValue, string localValue)
|
||||
{
|
||||
await ProcessSettingAsync(settingName, ORGANIZATION_PLUGIN_ID, organizationValue);
|
||||
|
||||
var recorder = new WarningRecorder();
|
||||
Program.LOGGER_FACTORY = recorder;
|
||||
|
||||
await ProcessSettingAsync(settingName, LOCAL_PLUGIN_ID, "nil");
|
||||
var warningsWithoutDeclaration = recorder.Warnings.ToList();
|
||||
|
||||
await ProcessSettingAsync(settingName, LOCAL_PLUGIN_ID, localValue);
|
||||
var warningsWithDeclaration = recorder.Warnings.Skip(warningsWithoutDeclaration.Count).ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(warningsWithoutDeclaration, Is.Empty, "A local plugin which does not declare the setting attempts nothing.");
|
||||
Assert.That(warningsWithDeclaration, Has.Count.EqualTo(1), "A local plugin which declares the setting of the organization has to be reported.");
|
||||
Assert.That(warningsWithDeclaration, Has.All.Contains(settingName).And.All.Contains(LOCAL_PLUGIN_ID.ToString()), "The warning has to name the setting and the plugin.");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lets a configuration plugin set one of the settings which ALocalPluginIsReportedOnlyForSettingsItDeclares covers.
|
||||
/// </summary>
|
||||
private static Task<bool> ProcessSettingAsync(string settingName, Guid pluginId, string luaValue) => settingName switch
|
||||
{
|
||||
PROHIBITED_PROVIDERS => ProcessProhibitedProvidersAsync(pluginId, luaValue),
|
||||
|
||||
_ => ProcessToolTableAsync(settingName, pluginId, luaValue, allowUserOverride: false),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Lets a configuration plugin set the prohibited providers, a setting which holds a set.
|
||||
/// </summary>
|
||||
private static async Task<bool> ProcessProhibitedProvidersAsync(Guid pluginId, string luaList)
|
||||
{
|
||||
var settings = await CreateSettingsAsync(PROHIBITED_PROVIDERS, luaList, allowUserOverride: false);
|
||||
return ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ProhibitedProviders, pluginId, settings, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lets a configuration plugin set the update interval, a setting which holds a single value.
|
||||
/// </summary>
|
||||
private static async Task<bool> ProcessUpdateIntervalAsync(Guid pluginId, string interval, bool allowUserOverride)
|
||||
{
|
||||
var settings = await CreateSettingsAsync("DataApp.UpdateInterval", $"\"{interval}\"", allowUserOverride);
|
||||
return ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateInterval, pluginId, settings, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lets a configuration plugin set one of the tool tables, the settings which hold a table of texts.
|
||||
/// </summary>
|
||||
/// <param name="settingName">The name of the tool table.</param>
|
||||
/// <param name="pluginId">The configuration plugin.</param>
|
||||
/// <param name="luaTable">The table in Lua notation, or nil for a plugin which does not declare it.</param>
|
||||
/// <param name="allowUserOverride">True when the plugin offers the table as an editable default instead of locking it.</param>
|
||||
private static async Task<bool> ProcessToolTableAsync(string settingName, Guid pluginId, string luaTable, bool allowUserOverride)
|
||||
{
|
||||
var settings = await CreateSettingsAsync(settingName, luaTable, allowUserOverride);
|
||||
return ManagedConfiguration.TryProcessConfiguration(x => x.Tools, ToolTableProperty(settingName), pluginId, settings, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the managed state of a tool table.
|
||||
/// </summary>
|
||||
private static ConfigMeta<DataTools, IDictionary<string, string>> GetToolTableMeta(string settingName)
|
||||
{
|
||||
if (!ManagedConfiguration.TryGet(x => x.Tools, ToolTableProperty(settingName), out var configMeta))
|
||||
throw new InvalidOperationException($"The tool table '{settingName}' is not registered.");
|
||||
|
||||
return configMeta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the property of a tool table, the way PluginConfiguration hands it to ManagedConfiguration.
|
||||
/// </summary>
|
||||
private static Expression<Func<DataTools, IDictionary<string, string>>> ToolTableProperty(string settingName) => settingName switch
|
||||
{
|
||||
LOCKED_TOOL_SETTINGS => x => x.LockedToolSettings,
|
||||
DEFAULT_TOOL_SETTINGS => x => x.DefaultToolSettings,
|
||||
MINIMUM_PROVIDER_CONFIDENCE => x => x.MinimumProviderConfidenceByToolId,
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(settingName), settingName, "This test knows no such tool table."),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Reads a tool table straight from the settings data.
|
||||
/// </summary>
|
||||
private Dictionary<string, string> ToolTableOf(string settingName) => settingName switch
|
||||
{
|
||||
LOCKED_TOOL_SETTINGS => this.settingsManager.ConfigurationData.Tools.LockedToolSettings,
|
||||
DEFAULT_TOOL_SETTINGS => this.settingsManager.ConfigurationData.Tools.DefaultToolSettings,
|
||||
MINIMUM_PROVIDER_CONFIDENCE => this.settingsManager.ConfigurationData.Tools.MinimumProviderConfidenceByToolId,
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(settingName), settingName, "This test knows no such tool table."),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sets the minimum confidence levels the user chose for the tools.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This assigns a new table instead of changing the present one: the settings data registered
|
||||
/// first in a test run shares its table with the default value of the setting, and changing it
|
||||
/// would change that default for every later test.
|
||||
/// </remarks>
|
||||
private void SetUsersConfidenceLevels(Dictionary<string, string> levels) => this.settingsManager.ConfigurationData.Tools.MinimumProviderConfidenceByToolId = new(levels, StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Chooses an entry which suits a tool table: a strict value of an organization, and a loose
|
||||
/// value of a local plugin.
|
||||
/// </summary>
|
||||
private static ToolTableEntry SampleEntryOf(string settingName) => settingName switch
|
||||
{
|
||||
MINIMUM_PROVIDER_CONFIDENCE => new("read_web_page", "HIGH", "NONE"),
|
||||
|
||||
_ => new("read_web_page.freeAddressChoice", "OFF", "ON"),
|
||||
};
|
||||
|
||||
private static string ToLuaTable(string key, string value) => $$"""{ ["{{key}}"] = "{{value}}" }""";
|
||||
|
||||
/// <summary>
|
||||
/// An entry of a tool table, with the value an organization configures and the one a local plugin configures.
|
||||
/// </summary>
|
||||
private sealed record ToolTableEntry(string Key, string OrganizationValue, string LocalValue);
|
||||
|
||||
/// <summary>
|
||||
/// Records the warnings ManagedConfiguration writes to the log.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every other category gets a logger which writes nowhere. A type which keeps its logger in a
|
||||
/// static field, and happens to be initialized during the test, would otherwise keep this recorder
|
||||
/// for the rest of the test run.
|
||||
/// </remarks>
|
||||
private sealed class WarningRecorder : ILoggerFactory, ILogger
|
||||
{
|
||||
private readonly List<string> warnings = [];
|
||||
|
||||
public IReadOnlyList<string> Warnings => this.warnings;
|
||||
|
||||
public ILogger CreateLogger(string categoryName) => categoryName == nameof(ManagedConfiguration) ? this : NullLogger.Instance;
|
||||
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => logLevel is LogLevel.Warning;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (logLevel is LogLevel.Warning)
|
||||
this.warnings.Add(formatter(state, exception));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the table of settings a configuration plugin declares.
|
||||
/// </summary>
|
||||
/// <param name="settingName">The name of the setting.</param>
|
||||
/// <param name="luaValue">The value of the setting in Lua notation.</param>
|
||||
/// <param name="allowUserOverride">True when the plugin offers the value as an editable default instead of locking it.</param>
|
||||
private static async Task<LuaTable> CreateSettingsAsync(string settingName, string luaValue, bool allowUserOverride)
|
||||
{
|
||||
var state = LuaState.Create();
|
||||
state.OpenBasicLibrary();
|
||||
state.OpenTableLibrary();
|
||||
|
||||
await state.DoStringAsync($$"""
|
||||
SETTINGS = {}
|
||||
SETTINGS["{{settingName}}"] = {{luaValue}}
|
||||
SETTINGS["{{settingName}}.AllowUserOverride"] = {{(allowUserOverride ? "true" : "false")}}
|
||||
""");
|
||||
|
||||
if (!state.Environment["SETTINGS"].TryRead<LuaTable>(out var settingsTable))
|
||||
throw new InvalidOperationException("The settings of this test are not a Lua table.");
|
||||
|
||||
return settingsTable;
|
||||
}
|
||||
}
|
||||
@@ -22,14 +22,17 @@ namespace AIStudio.Tests.Tools.PluginSystem;
|
||||
/// Briefing assistant stayed out of reach of every organization that way.<br/><br/>
|
||||
/// The test therefore configures every registered setting in one plugin and runs it for real. A dry
|
||||
/// run would not do: it reads the values but applies none of them, so it cannot tell which settings
|
||||
/// were read. The settings are reached through Program.SERVICE_PROVIDER, and the managed state of each
|
||||
/// setting is static, which is why this fixture does not run alongside others.
|
||||
/// were read. The same plugin, run once for an organization and once locally, shows that no way of
|
||||
/// reading a setting lets a local plugin take over what the organization manages. The settings are
|
||||
/// reached through Program.SERVICE_PROVIDER, and the managed state of each setting is static, which is
|
||||
/// why this fixture does not run alongside others.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public sealed class ManagedSettingsCoverageTests
|
||||
{
|
||||
private static readonly Guid PLUGIN_ID = Guid.Parse("cd426cd3-bcff-4d6f-87bd-2c3c0756aa7d");
|
||||
private static readonly Guid LOCAL_PLUGIN_ID = Guid.Parse("71f3b8e5-2d94-4c0a-a6e1-5b8c07d2f9a3");
|
||||
|
||||
/// <summary>
|
||||
/// The value configured for every setting holding a string.
|
||||
@@ -72,6 +75,7 @@ public sealed class ManagedSettingsCoverageTests
|
||||
private RustService rustService = null!;
|
||||
private ServiceProvider serviceProvider = null!;
|
||||
private IServiceProvider previousServiceProvider = null!;
|
||||
private Func<Guid, bool> previousIsOrganizationPlugin = null!;
|
||||
|
||||
[SetUp]
|
||||
public void CreateSettings()
|
||||
@@ -85,6 +89,8 @@ public sealed class ManagedSettingsCoverageTests
|
||||
this.previousServiceProvider = Program.SERVICE_PROVIDER;
|
||||
this.serviceProvider = new ServiceCollection().AddSingleton(settingsManager).AddSingleton(this.rustService).BuildServiceProvider();
|
||||
Program.SERVICE_PROVIDER = this.serviceProvider;
|
||||
|
||||
this.previousIsOrganizationPlugin = ManagedConfiguration.IsOrganizationPlugin;
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
@@ -96,6 +102,7 @@ public sealed class ManagedSettingsCoverageTests
|
||||
// long as it still reaches the settings of this test:
|
||||
//
|
||||
ManagedConfiguration.CleanupLeftOverManagedConfigurations([], new HashSet<Guid>());
|
||||
ManagedConfiguration.IsOrganizationPlugin = this.previousIsOrganizationPlugin;
|
||||
|
||||
Program.SERVICE_PROVIDER = this.previousServiceProvider;
|
||||
this.serviceProvider.Dispose();
|
||||
@@ -105,7 +112,7 @@ public sealed class ManagedSettingsCoverageTests
|
||||
[Test]
|
||||
public async Task EveryRegisteredSettingCanBeManagedByAConfigurationPlugin()
|
||||
{
|
||||
using var plugin = await RunPluginWithEveryRegisteredSettingAsync();
|
||||
using var plugin = await RunPluginWithEveryRegisteredSettingAsync(PLUGIN_ID);
|
||||
var registeredSettings = ManagedConfiguration.RegisteredSettings.ToList();
|
||||
var unmanagedSettingNames = registeredSettings
|
||||
.Where(setting => !EXEMPT_SETTINGS.ContainsKey(setting.SettingName) && !IsManagedByThePlugin(setting))
|
||||
@@ -124,7 +131,7 @@ public sealed class ManagedSettingsCoverageTests
|
||||
[Test]
|
||||
public async Task EveryExemptSettingIsRegisteredAndStaysUnmanaged()
|
||||
{
|
||||
using var plugin = await RunPluginWithEveryRegisteredSettingAsync();
|
||||
using var plugin = await RunPluginWithEveryRegisteredSettingAsync(PLUGIN_ID);
|
||||
var registeredSettings = ManagedConfiguration.RegisteredSettings.ToDictionary(setting => setting.SettingName, StringComparer.Ordinal);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
@@ -138,15 +145,47 @@ public sealed class ManagedSettingsCoverageTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks that a local configuration plugin takes over none of the settings an organization manages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every way of reading a setting from a configuration plugin has to ask whether that plugin may
|
||||
/// manage the setting before it changes anything. A way which forgets to ask lets any local plugin
|
||||
/// replace what the organization configured, as the tool tables once did. A local plugin declaring
|
||||
/// every setting after the organization did finds such a way, whichever setting it serves.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public async Task NoLocalPluginTakesOverASettingOfAnOrganization()
|
||||
{
|
||||
ManagedConfiguration.IsOrganizationPlugin = pluginId => pluginId == PLUGIN_ID;
|
||||
|
||||
using var organizationPlugin = await RunPluginWithEveryRegisteredSettingAsync(PLUGIN_ID);
|
||||
var settingsOfTheOrganization = ManagedConfiguration.RegisteredSettings.Where(IsManagedByThePlugin).ToList();
|
||||
|
||||
using var localPlugin = await RunPluginWithEveryRegisteredSettingAsync(LOCAL_PLUGIN_ID);
|
||||
var takenOverSettingNames = settingsOfTheOrganization
|
||||
.Where(setting => !IsManagedByThePlugin(setting))
|
||||
.Select(setting => setting.SettingName)
|
||||
.Order(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
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>
|
||||
/// Loads a configuration plugin which declares a valid value for every registered setting, and
|
||||
/// applies it the way AI Studio does when the plugin starts.
|
||||
/// </summary>
|
||||
/// <param name="pluginId">The ID of the configuration plugin.</param>
|
||||
/// <returns>The started configuration plugin.</returns>
|
||||
private static async Task<PluginConfiguration> RunPluginWithEveryRegisteredSettingAsync()
|
||||
private static async Task<PluginConfiguration> RunPluginWithEveryRegisteredSettingAsync(Guid pluginId)
|
||||
{
|
||||
var code = new StringBuilder($$"""
|
||||
ID = "{{PLUGIN_ID}}"
|
||||
ID = "{{pluginId}}"
|
||||
NAME = "Configuration for the tests"
|
||||
DESCRIPTION = "A configuration plugin which declares every managed setting."
|
||||
VERSION = "1.0.0"
|
||||
|
||||
Reference in new issue
Block a user