mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-16 16:03:37 +00:00
Kept overlapping settings operations from freezing the Document Analysis assistant (#970)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Verify (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
parent
b8d971c586
commit
0993517608
@ -184,7 +184,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
this.formChangeTimer.Elapsed += (_, _) =>
|
||||
{
|
||||
this.formChangeTimer.Stop();
|
||||
this.OnFormChange().Observe($"{nameof(AssistantBase<TSettings>)}: handling a form change");
|
||||
this.InvokeAsync(this.OnFormChange).Observe($"{nameof(AssistantBase<TSettings>)}: handling a form change");
|
||||
};
|
||||
|
||||
this.MightPreselectValues();
|
||||
|
||||
@ -104,13 +104,13 @@ else
|
||||
@T("Note: This setting only takes effect when this policy is exported and distributed via a configuration plugin to other users. When enabled, users will only see the document selection interface and cannot view or modify the policy details. This setting does NOT affect your local view - you will always see the full policy definition for policies you create.")
|
||||
</MudJustifiedText>
|
||||
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => this.IsNoPolicySelectedOrProtected)" RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.policyMinimumProviderConfidence)" SelectionUpdateAsync="@(async level => await this.PolicyMinimumConfidenceWasChangedAsync(level))" />
|
||||
<ConfigurationMinConfidenceSelection Disabled="@(() => this.IsNoPolicySelectedOrProtected)" RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.policyMinimumProviderConfidence)" SelectionUpdate="@this.PolicyMinimumConfidenceWasChanged" />
|
||||
|
||||
<ToolSelectionField Component="@this.Component" SelectedToolIds="@this.policyAllowedToolIds" SelectedToolIdsChanged="@this.PolicyAllowedToolsWasChangedAsync" Disabled="@this.IsNoPolicySelectedOrProtected" Label="@T("Tools this policy permits")" Help="@T("Only the tools selected here can be used by the AI for an analysis with this policy. Every tool still has to meet the confidence requirements of the selected provider, so a tool may remain unavailable even when this policy permits it.")"/>
|
||||
<ToolSelectionField Component="@this.Component" SelectedToolIds="@this.policyAllowedToolIds" SelectedToolIdsChanged="@this.PolicyAllowedToolsWasChanged" Disabled="@this.IsNoPolicySelectedOrProtected" Label="@T("Tools this policy permits")" Help="@T("Only the tools selected here can be used by the AI for an analysis with this policy. Every tool still has to meet the confidence requirements of the selected provider, so a tool may remain unavailable even when this policy permits it.")"/>
|
||||
|
||||
<ConfigurationProviderSelection Component="Components.DOCUMENT_ANALYSIS_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => this.IsNoPolicySelectedOrProtected)" SelectedValue="@(() => this.policyPreselectedProviderId)" SelectionUpdate="@this.PolicyPreselectedProviderWasChanged" ExplicitMinimumConfidence="@this.GetPolicyMinimumConfidenceLevel()"/>
|
||||
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => this.IsNoPolicySelected)" SelectedValue="@(() => this.policyPreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdateAsync="@(async selection => await this.PolicyPreselectedProfileWasChangedAsync(selection))" OptionHelp="@T("Choose whether the policy should use the app default profile, no profile, or a specific profile.")"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => this.IsNoPolicySelected)" SelectedValue="@(() => this.policyPreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@this.PolicyPreselectedProfileWasChanged" OptionHelp="@T("Choose whether the policy should use the app default profile, no profile, or a specific profile.")"/>
|
||||
|
||||
<MudTextSwitch Disabled="@(this.IsNoPolicySelected || (this.selectedPolicy?.IsEnterpriseConfiguration ?? true))" Label="@T("Would you like to protect this policy so that you cannot accidentally edit or delete it?")" Value="@this.policyIsProtected" ValueChanged="async state => await this.PolicyProtectionWasChanged(state)" LabelOn="@T("Yes, protect this policy")" LabelOff="@T("No, the policy can be edited")" />
|
||||
|
||||
|
||||
@ -255,33 +255,80 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
|
||||
private async Task AutoSave(bool force = false)
|
||||
{
|
||||
if(this.selectedPolicy is null)
|
||||
//
|
||||
// A pending store is a property of the settings, not of the selected policy: the value has
|
||||
// been written into its policy already, so what is still outstanding is the store itself.
|
||||
// It therefore outlives a form reset and a switch to another policy, and only a completed
|
||||
// store clears it.
|
||||
//
|
||||
var hasChanges = this.policyStorePending;
|
||||
if(this.selectedPolicy is { } policy)
|
||||
hasChanges |= this.ApplyFormToPolicy(policy, force);
|
||||
|
||||
if (!hasChanges)
|
||||
return;
|
||||
|
||||
// The preselected profile is always user-adjustable, even for protected policies and enterprise configurations:
|
||||
this.selectedPolicy.PreselectedProfile = this.policyPreselectedProfile;
|
||||
|
||||
// Enterprise configurations cannot be modified at all:
|
||||
if(this.selectedPolicy.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
var canEditProtectedFields = force || (!this.selectedPolicy.IsProtected && !this.policyIsProtected);
|
||||
if (canEditProtectedFields)
|
||||
{
|
||||
this.selectedPolicy.PreselectedProvider = this.policyPreselectedProviderId;
|
||||
this.selectedPolicy.PolicyName = this.policyName;
|
||||
this.selectedPolicy.PolicyDescription = this.policyDescription;
|
||||
this.selectedPolicy.IsProtected = this.policyIsProtected;
|
||||
this.selectedPolicy.HidePolicyDefinition = this.policyHidePolicyDefinition;
|
||||
this.selectedPolicy.AnalysisRules = this.policyAnalysisRules;
|
||||
this.selectedPolicy.OutputRules = this.policyOutputRules;
|
||||
this.selectedPolicy.MinimumProviderConfidence = this.policyMinimumProviderConfidence;
|
||||
this.selectedPolicy.AllowedToolIds = [..this.policyAllowedToolIds];
|
||||
}
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
this.policyStorePending = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the form values over into the given policy.
|
||||
/// </summary>
|
||||
/// <param name="policy">The policy to write the form values to.</param>
|
||||
/// <param name="force">Whether the protected fields may be written as well.</param>
|
||||
/// <returns>True when this changed anything about the policy, false otherwise.</returns>
|
||||
private bool ApplyFormToPolicy(DataDocumentAnalysisPolicy policy, bool force)
|
||||
{
|
||||
// The preselected profile is always user-adjustable, even for protected policies and enterprise configurations:
|
||||
var hasChanges = policy.PreselectedProfile != this.policyPreselectedProfile;
|
||||
policy.PreselectedProfile = this.policyPreselectedProfile;
|
||||
|
||||
// Enterprise configurations cannot be modified at all:
|
||||
if(policy.IsEnterpriseConfiguration)
|
||||
return hasChanges;
|
||||
|
||||
var canEditProtectedFields = force || (!policy.IsProtected && !this.policyIsProtected);
|
||||
if (!canEditProtectedFields)
|
||||
return hasChanges;
|
||||
|
||||
hasChanges = hasChanges
|
||||
|| policy.PolicyName != this.policyName
|
||||
|| policy.PreselectedProvider != this.policyPreselectedProviderId
|
||||
|| policy.PolicyDescription != this.policyDescription
|
||||
|| policy.IsProtected != this.policyIsProtected
|
||||
|| policy.HidePolicyDefinition != this.policyHidePolicyDefinition
|
||||
|| policy.AnalysisRules != this.policyAnalysisRules
|
||||
|| policy.OutputRules != this.policyOutputRules
|
||||
|| policy.MinimumProviderConfidence != this.policyMinimumProviderConfidence
|
||||
|| !policy.AllowedToolIds.SetEquals(this.policyAllowedToolIds);
|
||||
|
||||
policy.PreselectedProvider = this.policyPreselectedProviderId;
|
||||
policy.PolicyName = this.policyName;
|
||||
policy.PolicyDescription = this.policyDescription;
|
||||
policy.IsProtected = this.policyIsProtected;
|
||||
policy.HidePolicyDefinition = this.policyHidePolicyDefinition;
|
||||
policy.AnalysisRules = this.policyAnalysisRules;
|
||||
policy.OutputRules = this.policyOutputRules;
|
||||
policy.MinimumProviderConfidence = this.policyMinimumProviderConfidence;
|
||||
policy.AllowedToolIds = [..this.policyAllowedToolIds];
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the given policy may take over an edit of one of its protected fields right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The handlers which write their value straight into the policy have to ask this themselves.
|
||||
/// ApplyFormToPolicy asks the same question, but it never gets to judge their fields: they have
|
||||
/// already brought policy and form in line, so nothing is left for it to compare. The markup
|
||||
/// disables those controls for a protected policy and an enterprise policy is always a protected
|
||||
/// one, which is why nobody should ever reach a handler that way -- this keeps the rule in the
|
||||
/// code as well, where the next handler will look for it. The form value counts alongside the
|
||||
/// stored one, because the protection switch is flipped before the store which writes it has run.
|
||||
/// </remarks>
|
||||
private bool AcceptsProtectedFieldEdits(DataDocumentAnalysisPolicy policy) => policy is { IsEnterpriseConfiguration: false, IsProtected: false } && !this.policyIsProtected;
|
||||
|
||||
private DataDocumentAnalysisPolicy? selectedPolicy;
|
||||
private bool policyIsProtected;
|
||||
private bool policyHidePolicyDefinition;
|
||||
@ -298,6 +345,21 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
/// </remarks>
|
||||
private bool documentSelectionExpanded;
|
||||
private string policyName = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether an edit already applied to a policy still waits to be written to the settings file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Some handlers apply their value to the selected policy at once, because the rest of the
|
||||
/// assistant reads it back from there right away: the policy list has to show a new name while
|
||||
/// it is being typed, and the provider preselection is recomputed from the policy, not from the
|
||||
/// form. Doing so leaves the auto-save nothing to compare the form against -- form and policy
|
||||
/// already agree -- so every such handler has to announce the store itself. That is what this
|
||||
/// flag is for. It belongs to no particular policy: the value has long arrived where it
|
||||
/// belongs, only the file has not caught up yet, which is why a form reset or a switch to
|
||||
/// another policy does not clear it. Only a completed store does.
|
||||
/// </remarks>
|
||||
private bool policyStorePending;
|
||||
private string policyDescription = string.Empty;
|
||||
private string policyAnalysisRules = string.Empty;
|
||||
private string policyOutputRules = string.Empty;
|
||||
@ -477,6 +539,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
return;
|
||||
|
||||
this.selectedPolicy.PolicyName = this.policyName;
|
||||
this.policyStorePending = true;
|
||||
}
|
||||
|
||||
private async Task PolicyProtectionWasChanged(bool state)
|
||||
@ -488,7 +551,6 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
return;
|
||||
|
||||
this.policyIsProtected = state;
|
||||
this.selectedPolicy.IsProtected = state;
|
||||
this.policyDefinitionExpanded = !state;
|
||||
this.documentSelectionExpanded = state;
|
||||
await this.AutoSave(true);
|
||||
@ -503,7 +565,6 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
return;
|
||||
|
||||
this.policyHidePolicyDefinition = state;
|
||||
this.selectedPolicy.HidePolicyDefinition = state;
|
||||
await this.AutoSave(true);
|
||||
}
|
||||
|
||||
@ -580,39 +641,50 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
/// <summary>
|
||||
/// Takes over the tools this policy permits.
|
||||
/// </summary>
|
||||
private async Task PolicyAllowedToolsWasChangedAsync(HashSet<string> allowedToolIds)
|
||||
private void PolicyAllowedToolsWasChanged(HashSet<string> allowedToolIds)
|
||||
{
|
||||
this.policyAllowedToolIds = allowedToolIds;
|
||||
await this.AutoSave();
|
||||
if (this.selectedPolicy is not { } policy || !this.AcceptsProtectedFieldEdits(policy))
|
||||
return;
|
||||
|
||||
policy.AllowedToolIds = [..allowedToolIds];
|
||||
this.policyStorePending = true;
|
||||
}
|
||||
|
||||
private async Task PolicyMinimumConfidenceWasChangedAsync(ConfidenceLevel level)
|
||||
private void PolicyMinimumConfidenceWasChanged(ConfidenceLevel level)
|
||||
{
|
||||
this.policyMinimumProviderConfidence = level;
|
||||
await this.AutoSave();
|
||||
if (this.selectedPolicy is { } policy && this.AcceptsProtectedFieldEdits(policy))
|
||||
{
|
||||
policy.MinimumProviderConfidence = level;
|
||||
this.policyStorePending = true;
|
||||
}
|
||||
|
||||
this.ApplyPolicyPreselection();
|
||||
}
|
||||
|
||||
private void PolicyPreselectedProviderWasChanged(string providerId)
|
||||
{
|
||||
if (this.selectedPolicy is null)
|
||||
if (this.selectedPolicy is not { } policy || !this.AcceptsProtectedFieldEdits(policy))
|
||||
return;
|
||||
|
||||
this.policyPreselectedProviderId = providerId;
|
||||
this.selectedPolicy.PreselectedProvider = providerId;
|
||||
policy.PreselectedProvider = providerId;
|
||||
this.policyStorePending = true;
|
||||
this.ProviderSettings = Settings.Provider.NONE;
|
||||
this.ApplyPolicyPreselection();
|
||||
}
|
||||
|
||||
private async Task PolicyPreselectedProfileWasChangedAsync(ProfilePreselection selection)
|
||||
private void PolicyPreselectedProfileWasChanged(ProfilePreselection selection)
|
||||
{
|
||||
this.policyPreselectedProfile = selection;
|
||||
if (this.selectedPolicy is not null)
|
||||
{
|
||||
this.selectedPolicy.PreselectedProfile = this.policyPreselectedProfile;
|
||||
this.policyStorePending = true;
|
||||
}
|
||||
|
||||
this.CurrentProfile = this.ResolveProfileSelection();
|
||||
await this.AutoSave();
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
@ -4216,6 +4216,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T3654011106"] = "Open P
|
||||
-- You can switch between your profiles here
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T918741365"] = "You can switch between your profiles here"
|
||||
|
||||
-- No LLM providers are configured yet. Add a provider in the app settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1166628228"] = "No LLM providers are configured yet. Add a provider in the app settings."
|
||||
|
||||
-- No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1220991024"] = "No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings."
|
||||
|
||||
-- Audio input possible
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1742581112"] = "Audio input possible"
|
||||
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
@using AIStudio.Settings
|
||||
@inherits MSGComponentBase
|
||||
@{
|
||||
var availableProviderItems = this.GetAvailableProviderSelectionItems().ToList();
|
||||
}
|
||||
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Margin="Margin.Dense" Label="@T("Provider")" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined" Disabled="@this.Disabled">
|
||||
@foreach (var providerItem in this.GetAvailableProviderSelectionItems())
|
||||
@foreach (var providerItem in availableProviderItems)
|
||||
{
|
||||
<MudSelectItem Value="@providerItem.Provider">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="w-100" Wrap="Wrap.NoWrap">
|
||||
@ -21,3 +24,9 @@
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (availableProviderItems.Count is 0 && this.GetEmptySelectionHint() is { } emptySelectionHint)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Error" Class="mb-3">
|
||||
@emptySelectionHint
|
||||
</MudText>
|
||||
}
|
||||
|
||||
@ -53,6 +53,28 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
yield return new(provider, this.GetCapabilityIcons(provider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Says why there is nothing to choose from, or nothing at all when that is not the user's doing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An empty list has two causes the user can act on, and they lead to different places in the
|
||||
/// settings: there is no provider yet, or none of the configured ones reaches the confidence
|
||||
/// this component asks for. Naming the wrong one sends the user looking in the wrong place --
|
||||
/// a first start has nobody to blame for a confidence level it never set. A missing or invalid
|
||||
/// component is a third case and neither of those: it is a defect, it was logged as one, and
|
||||
/// any explanation offered to the user here would be a guess.
|
||||
/// </remarks>
|
||||
private string? GetEmptySelectionHint()
|
||||
{
|
||||
if (this.Component is null or Tools.Components.NONE)
|
||||
return null;
|
||||
|
||||
if (!this.SettingsManager.GetAllProviders().Any(x => x.UsedLLMProvider is not LLMProviders.NONE))
|
||||
return this.T("No LLM providers are configured yet. Add a provider in the app settings.");
|
||||
|
||||
return this.T("No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings.");
|
||||
}
|
||||
|
||||
private IReadOnlyList<CapabilityIcon> GetCapabilityIcons(AIStudio.Settings.Provider provider)
|
||||
{
|
||||
var profile = provider.GetModelProfile();
|
||||
|
||||
@ -4218,6 +4218,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T3654011106"] = "Profil
|
||||
-- You can switch between your profiles here
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T918741365"] = "Hier können Sie zwischen ihren Profilen wechseln."
|
||||
|
||||
-- No LLM providers are configured yet. Add a provider in the app settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1166628228"] = "Bisher wurden keine LLM-Anbieter konfiguriert. Bitte fügen Sie einen Anbieter in den App-Einstellungen hinzu."
|
||||
|
||||
-- No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1220991024"] = "Kein LLM-Anbieter erfüllt die Vertrauensanforderungen. Bitte konfigurieren Sie einen geeigneten Anbieter in den App-Einstellungen."
|
||||
|
||||
-- Audio input possible
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1742581112"] = "Audioeingabe möglich"
|
||||
|
||||
@ -11598,6 +11604,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINTYPEEXTENSIONS::T335338363
|
||||
-- Standard augmentation process
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T1072508429"] = "Standardmäßiger Erweiterungsprozess"
|
||||
|
||||
-- No provider is trusted enough to check which passages fit your question. This answer uses all passages that were found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T2710880477"] = "Kein Anbieter ist vertrauenswürdig genug, um zu prüfen, welche Abschnitte zu Ihrer Frage passen. Diese Antwort verwendet alle gefundenen Abschnitte."
|
||||
|
||||
-- This is the standard augmentation process, which uses all retrieval contexts to augment the chat thread.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T3240406069"] = "Dies ist der Standard-Erweiterungsprozess, bei dem alle abgerufenen Kontexte verwendet werden, um den Chatverlauf zu ergänzen."
|
||||
|
||||
@ -11610,6 +11619,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::DATASOURCESELECTIONPROCESSES::AGENTICSRCS
|
||||
-- Automatically selects the appropriate data sources based on the last prompt. Applies a heuristic reduction at the end to reduce the number of data sources.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::DATASOURCESELECTIONPROCESSES::AGENTICSRCSELWITHDYNHEUR::T648937779"] = "Wählt automatisch die passenden Datenquellen basierend auf der letzten Eingabe aus. Wendet am Ende eine heuristische Reduzierung an, um die Anzahl der Datenquellen zu verringern."
|
||||
|
||||
-- None of your selected data sources is available for the chosen provider. This answer was created without them.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T1696726639"] = "Keine der ausgewählten Datenquellen ist für den gewählten Anbieter verfügbar. Diese Antwort wurde ohne sie erstellt."
|
||||
|
||||
-- This RAG process filters data sources, automatically selects appropriate sources, optionally allows manual source selection, retrieves data, and automatically validates the retrieval context.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T3047786484"] = "Dieser RAG-Prozess filtert Datenquellen, wählt automatisch passende Quellen aus, ermöglicht optional die manuelle Auswahl von Quellen, ruft Daten ab und überprüft den Abrufkontext automatisch."
|
||||
|
||||
|
||||
@ -4218,6 +4218,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T3654011106"] = "Open P
|
||||
-- You can switch between your profiles here
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T918741365"] = "You can switch between your profiles here"
|
||||
|
||||
-- No LLM providers are configured yet. Add a provider in the app settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1166628228"] = "No LLM providers are configured yet. Add a provider in the app settings."
|
||||
|
||||
-- No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1220991024"] = "No LLM providers meet the confidence requirements. Configure an eligible provider in the app settings."
|
||||
|
||||
-- Audio input possible
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1742581112"] = "Audio input possible"
|
||||
|
||||
@ -11598,6 +11604,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINTYPEEXTENSIONS::T335338363
|
||||
-- Standard augmentation process
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T1072508429"] = "Standard augmentation process"
|
||||
|
||||
-- No provider is trusted enough to check which passages fit your question. This answer uses all passages that were found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T2710880477"] = "No provider is trusted enough to check which passages fit your question. This answer uses all passages that were found."
|
||||
|
||||
-- This is the standard augmentation process, which uses all retrieval contexts to augment the chat thread.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::AUGMENTATIONPROCESSES::AUGMENTATIONONE::T3240406069"] = "This is the standard augmentation process, which uses all retrieval contexts to augment the chat thread."
|
||||
|
||||
@ -11610,6 +11619,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::DATASOURCESELECTIONPROCESSES::AGENTICSRCS
|
||||
-- Automatically selects the appropriate data sources based on the last prompt. Applies a heuristic reduction at the end to reduce the number of data sources.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::DATASOURCESELECTIONPROCESSES::AGENTICSRCSELWITHDYNHEUR::T648937779"] = "Automatically selects the appropriate data sources based on the last prompt. Applies a heuristic reduction at the end to reduce the number of data sources."
|
||||
|
||||
-- None of your selected data sources is available for the chosen provider. This answer was created without them.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T1696726639"] = "None of your selected data sources is available for the chosen provider. This answer was created without them."
|
||||
|
||||
-- This RAG process filters data sources, automatically selects appropriate sources, optionally allows manual source selection, retrieves data, and automatically validates the retrieval context.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RAG::RAGPROCESSES::AISRCSELWITHRETCTXVAL::T3047786484"] = "This RAG process filters data sources, automatically selects appropriate sources, optionally allows manual source selection, retrieves data, and automatically validates the retrieval context."
|
||||
|
||||
|
||||
@ -34,6 +34,22 @@ public sealed class SettingsManager
|
||||
private readonly ILogger<SettingsManager> logger;
|
||||
private readonly RustService rustService;
|
||||
|
||||
/// <summary>
|
||||
/// Lets only one operation at a time touch the settings files.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reading takes this as well as writing does, for two reasons. A read migrates and backs up
|
||||
/// what it found, so it writes the very files a store writes. And it re-evaluates whether
|
||||
/// writes are blocked at all, starting out by clearing that block: a store slipping through
|
||||
/// that moment would overwrite the settings the block exists to protect.<br/><br/>
|
||||
/// What this does not do is guard the settings themselves. It guards the files: what one store
|
||||
/// writes, the next one no longer has to fear. The configuration data behind them stays open to
|
||||
/// everybody, and a store serializes it while the rest of the app goes on editing it -- a list
|
||||
/// growing mid-serialization still throws. Whoever wants that answered needs one of their own;
|
||||
/// this lock is not it.
|
||||
/// </remarks>
|
||||
private readonly SemaphoreSlim settingsFileSemaphore = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// The settings manager.
|
||||
/// </summary>
|
||||
@ -103,6 +119,19 @@ public sealed class SettingsManager
|
||||
/// </summary>
|
||||
/// <returns>A (migrated) settings snapshot, or null if it could not be read.</returns>
|
||||
public async Task<Data?> TryReadSettingsSnapshot()
|
||||
{
|
||||
await this.settingsFileSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
return await this.ReadSettingsSnapshot();
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.settingsFileSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Data?> ReadSettingsSnapshot()
|
||||
{
|
||||
this.SettingsWriteBlockReason = SettingsWriteBlockReason.NONE;
|
||||
if(!this.IsSetUp)
|
||||
@ -294,41 +323,72 @@ public sealed class SettingsManager
|
||||
/// </summary>
|
||||
public async Task StoreSettings()
|
||||
{
|
||||
if(!this.IsSetUp)
|
||||
await this.settingsFileSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
|
||||
return;
|
||||
}
|
||||
if(!this.IsSetUp)
|
||||
{
|
||||
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.SettingsWriteBlocked)
|
||||
if(this.SettingsWriteBlocked)
|
||||
{
|
||||
this.logger.LogWarning($"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
|
||||
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
|
||||
await this.StoreSerializedSettings(settingsJson, settingsPath);
|
||||
await this.StoreSerializedVersionBackup(this.ConfigurationData.Version, settingsJson);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.logger.LogWarning($"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'.");
|
||||
return;
|
||||
this.settingsFileSemaphore.Release();
|
||||
}
|
||||
|
||||
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
|
||||
await this.StoreSettingsSnapshot(this.ConfigurationData, settingsPath);
|
||||
await this.StoreCurrentVersionBackup(this.ConfigurationData);
|
||||
}
|
||||
|
||||
private static string GetBackupSettingsFilename(Version version) => $"settings.{version.ToString().ToLowerInvariant()}.json";
|
||||
|
||||
private static string GetBackupSettingsPath(Version version) => Path.Combine(ConfigDirectory!, GetBackupSettingsFilename(version));
|
||||
|
||||
private async Task StoreCurrentVersionBackup(Data settingsData)
|
||||
private Task StoreCurrentVersionBackup(Data settingsData) =>
|
||||
this.StoreSerializedVersionBackup(settingsData.Version, JsonSerializer.Serialize(settingsData, JSON_OPTIONS));
|
||||
|
||||
/// <summary>
|
||||
/// Writes the backup file from settings which were serialized already.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The store hands the same JSON to this method and to the one writing the settings file, so
|
||||
/// that both files say the same thing. Serializing twice cannot promise that: the configuration
|
||||
/// data may well have changed in between, and the backup would then describe a state the
|
||||
/// settings file never had.
|
||||
/// </remarks>
|
||||
/// <param name="settingsVersion">The version the serialized settings carry.</param>
|
||||
/// <param name="settingsJson">The serialized settings.</param>
|
||||
private async Task StoreSerializedVersionBackup(Version settingsVersion, string settingsJson)
|
||||
{
|
||||
if(settingsData.Version != CURRENT_SETTINGS_VERSION)
|
||||
if(settingsVersion != CURRENT_SETTINGS_VERSION)
|
||||
{
|
||||
this.logger.LogWarning($"Skipping settings backup because the settings version '{settingsData.Version}' is not the current version '{CURRENT_SETTINGS_VERSION}'.");
|
||||
this.logger.LogWarning($"Skipping settings backup because the settings version '{settingsVersion}' is not the current version '{CURRENT_SETTINGS_VERSION}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
var backupSettingsPath = GetBackupSettingsPath(CURRENT_SETTINGS_VERSION);
|
||||
await this.StoreSettingsSnapshot(settingsData, backupSettingsPath);
|
||||
await this.StoreSerializedSettings(settingsJson, backupSettingsPath);
|
||||
this.logger.LogInformation($"Stored the settings backup file '{backupSettingsPath}'.");
|
||||
}
|
||||
|
||||
private async Task StoreSettingsSnapshot(Data settingsData, string settingsPath)
|
||||
private Task StoreSettingsSnapshot(Data settingsData, string settingsPath) =>
|
||||
this.StoreSerializedSettings(JsonSerializer.Serialize(settingsData, JSON_OPTIONS), settingsPath);
|
||||
|
||||
/// <summary>
|
||||
/// Writes settings which were serialized already to the given path.
|
||||
/// </summary>
|
||||
/// <param name="settingsJson">The serialized settings.</param>
|
||||
/// <param name="settingsPath">The file to write them to.</param>
|
||||
private async Task StoreSerializedSettings(string settingsJson, string settingsPath)
|
||||
{
|
||||
if(!Directory.Exists(ConfigDirectory))
|
||||
{
|
||||
@ -336,8 +396,6 @@ public sealed class SettingsManager
|
||||
Directory.CreateDirectory(ConfigDirectory!);
|
||||
}
|
||||
|
||||
var settingsJson = JsonSerializer.Serialize(settingsData, JSON_OPTIONS);
|
||||
|
||||
//
|
||||
// We write the new settings next to the previous ones and replace them afterwards, so that
|
||||
// no crash can leave a half-written settings file behind. The temporary file has to live in
|
||||
|
||||
@ -29,8 +29,11 @@
|
||||
- Improved how AI Studio works out what a model can do. Every model family now stands on its own, together with the page it was read from, and our build refuses rules which contradict each other or name no source. That way, mistakes are caught before they ever reach you.
|
||||
- Improved the list of your attached files: every file now appears under the folder it came from, and each folder is named only once, no matter in which order you attached your files.
|
||||
- Improved organization-wide provider management: IT departments can now separately prevent users from adding chat, transcription, or embedding providers. The existing master setting still overrides all three provider-specific settings.
|
||||
- Improved the provider selection throughout the assistants: when there is nothing to choose from, it now says why. Either you have not set up a provider yet, or none of yours is trusted enough for what you are doing. Before, the list was simply empty.
|
||||
- Changed how provider trust and provider confidence work together. Marking a provider as trustworthy in a configuration no longer also satisfies a required confidence level: one says who runs the provider, the other how confidential it is. Organizations raise a provider's level in their own confidence scheme instead. This applies beyond local data sources, for example, when a model reads a page from your intranet.
|
||||
- Fixed the abilities AI Studio assumed for many models. We checked the families against their documentation: some models gained image input, reasoning, or tool calling, others lost an ability they never had.
|
||||
- Fixed the Document Analysis assistant freezing while you edited a policy. It needed a change of yours to be saved in the background just as you were making the next one — picking a provider, for instance — which is why it hit some of you again and again and others never at all.
|
||||
- Fixed a renamed policy losing its new name in the Document Analysis assistant. The name was kept only when you happened to change something else afterward.
|
||||
- Fixed model names that a provider writes in its own way not being recognized at all, such as the colon Ollama puts before the variant. Those models were treated as plain text models and lost every other ability.
|
||||
- Fixed a model resold under a plain name not getting the abilities it really has.
|
||||
- Fixed image and video generation models showing up among the chat models.
|
||||
|
||||
159
app/Tests/Settings/SettingsStorageTests.cs
Normal file
159
app/Tests/Settings/SettingsStorageTests.cs
Normal file
@ -0,0 +1,159 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
using Version = AIStudio.Settings.Version;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Checks what settings operations do to each other when they overlap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The settings are written from everywhere: a timer firing on its own thread, a dialog the user
|
||||
/// just closed, a configuration plugin which arrived over the network. Nothing keeps two of those
|
||||
/// from meeting, and what they must never leave behind is a settings file nobody can read -- it is
|
||||
/// the file the app starts from the next morning. These tests arrange the meeting on purpose and
|
||||
/// look at what is on the disk afterward.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public sealed class SettingsStorageTests
|
||||
{
|
||||
/// <summary>
|
||||
/// How many operations are set against each other.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// High enough that the operations genuinely overlap on any machine, low enough that the test
|
||||
/// stays a test. A race which needs more than this to show up would not be one the app meets.
|
||||
/// </remarks>
|
||||
private const int CONCURRENT_OPERATIONS = 50;
|
||||
|
||||
private const string SETTINGS_FILENAME = "settings.json";
|
||||
|
||||
private const string BACKUP_FILENAME = "settings.v6.json";
|
||||
|
||||
private string? previousConfigDirectory;
|
||||
private string? previousDataDirectory;
|
||||
private string testDirectory = string.Empty;
|
||||
|
||||
[SetUp]
|
||||
public void PrepareTestDirectory()
|
||||
{
|
||||
//
|
||||
// Both directories are static state of the whole application, which is why this fixture
|
||||
// does not run alongside others. They are put back in the teardown so that a later test
|
||||
// does not inherit a directory which is gone by then.
|
||||
//
|
||||
this.previousConfigDirectory = SettingsManager.ConfigDirectory;
|
||||
this.previousDataDirectory = SettingsManager.DataDirectory;
|
||||
|
||||
this.testDirectory = Path.Combine(Path.GetTempPath(), $"ai-studio-settings-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(this.testDirectory);
|
||||
|
||||
SettingsManager.ConfigDirectory = this.testDirectory;
|
||||
SettingsManager.DataDirectory = this.testDirectory;
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void RemoveTestDirectory()
|
||||
{
|
||||
SettingsManager.ConfigDirectory = this.previousConfigDirectory;
|
||||
SettingsManager.DataDirectory = this.previousDataDirectory;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(this.testDirectory, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// A temporary directory we could not remove says nothing about the code under test.
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OverlappingStoresLeaveBothFilesReadable()
|
||||
{
|
||||
var settingsManager = CreateSettingsManager();
|
||||
await Task.WhenAll(Enumerable.Range(0, CONCURRENT_OPERATIONS).Select(_ => settingsManager.StoreSettings()));
|
||||
|
||||
var settingsPath = Path.Combine(this.testDirectory, SETTINGS_FILENAME);
|
||||
var backupPath = Path.Combine(this.testDirectory, BACKUP_FILENAME);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(File.Exists(settingsPath), Is.True, "The settings file was never written.");
|
||||
Assert.That(File.Exists(backupPath), Is.True, "The settings backup file was never written.");
|
||||
Assert.That(ReadSettingsFile(settingsPath)?.Version, Is.EqualTo(Version.V6), "The settings file could not be read back.");
|
||||
Assert.That(ReadSettingsFile(backupPath)?.Version, Is.EqualTo(Version.V6), "The settings backup file could not be read back.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OverlappingStoresLeaveNoTemporaryFilesBehind()
|
||||
{
|
||||
var settingsManager = CreateSettingsManager();
|
||||
await Task.WhenAll(Enumerable.Range(0, CONCURRENT_OPERATIONS).Select(_ => settingsManager.StoreSettings()));
|
||||
|
||||
//
|
||||
// Every store writes its settings next to the previous ones and renames afterwards. The
|
||||
// temporary file carries a name of its own, so two stores cannot collide over it -- but a
|
||||
// store which gave up halfway would leave one lying around, and the next start would find
|
||||
// a configuration directory filling up with them.
|
||||
//
|
||||
var leftovers = Directory.GetFiles(this.testDirectory, "*.tmp-*").Select(Path.GetFileName).ToList();
|
||||
Assert.That(leftovers, Is.Empty, $"Temporary settings files were left behind: {string.Join(", ", leftovers)}.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AStoreCannotSlipThroughWhileAReadReconsidersTheWriteBlock()
|
||||
{
|
||||
var settingsPath = Path.Combine(this.testDirectory, SETTINGS_FILENAME);
|
||||
|
||||
//
|
||||
// Settings written by a newer app than this one. Reading them blocks every write, so that
|
||||
// this app cannot replace settings it does not understand with the little it does. What
|
||||
// makes this the interesting case is how a read arrives at that verdict: it clears the
|
||||
// block first and only re-establishes it once it has seen the file. A store meeting that
|
||||
// moment would find nothing standing in its way and overwrite the very file the block
|
||||
// exists for -- which is why a read holds the same lock a store does.
|
||||
//
|
||||
await File.WriteAllTextAsync(settingsPath, """{"Version": "V99"}""");
|
||||
|
||||
var settingsManager = CreateSettingsManager();
|
||||
await settingsManager.TryReadSettingsSnapshot();
|
||||
Assert.That(settingsManager.SettingsWriteBlockReason, Is.EqualTo(SettingsWriteBlockReason.VERSION_NEWER_THAN_APP), "The newer settings file did not block writes in the first place.");
|
||||
|
||||
var operations = new List<Task>();
|
||||
for (var i = 0; i < CONCURRENT_OPERATIONS; i++)
|
||||
{
|
||||
operations.Add(settingsManager.StoreSettings());
|
||||
operations.Add(settingsManager.TryReadSettingsSnapshot());
|
||||
}
|
||||
|
||||
await Task.WhenAll(operations);
|
||||
|
||||
using var settingsDocument = JsonDocument.Parse(await File.ReadAllTextAsync(settingsPath));
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(settingsDocument.RootElement.GetProperty("Version").GetString(), Is.EqualTo("V99"), "A store overwrote the newer settings file while a read was reconsidering the write block.");
|
||||
Assert.That(settingsManager.SettingsWriteBlockReason, Is.EqualTo(SettingsWriteBlockReason.VERSION_NEWER_THAN_APP), "The write block did not survive the reads which re-established it.");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a settings manager the way these tests need it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The rust service is handed in as null on purpose: neither storing nor reading settings ever
|
||||
/// asks it anything. Only the active language is read through it, and that is not what is being
|
||||
/// checked here. Should a future store reach for it, the test says so by failing loudly rather
|
||||
/// than by quietly testing a different thing.
|
||||
/// </remarks>
|
||||
private static SettingsManager CreateSettingsManager() => new(NullLogger<SettingsManager>.Instance, null!);
|
||||
|
||||
private static Data? ReadSettingsFile(string settingsPath) => JsonSerializer.Deserialize<Data>(File.ReadAllText(settingsPath), SettingsManager.JSON_OPTIONS);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user