Fixed plugin values which depended on the region settings of the computer (#1194)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-12 08:57:50 +01:00
1 parent 65d50de033
commit a1b493cb13
20 files changed
+397 -137

No files matched your search

+26
View File
@@ -84,6 +84,32 @@ public sealed class LuaTableTests
""".ReplaceLineEndings("\n")));
}
/// <summary>
/// The order of the texts must not depend on the region settings of the computer which writes them.
/// </summary>
/// <remarks>
/// A comparison by culture sorts the underscore before the letters, the ordinal one after them. The
/// language plugins were written in the first order before, so this test fails with it.
/// </remarks>
[Test]
[SetCulture("de-DE")]
public void TheTextsAreOrderedOrdinallyWhateverTheCulture()
{
var code = Create(new Dictionary<string, string>
{
["UI_TEXT_CONTENT.AISTUDIO.DIALOGS.A_B.T1"] = "With an underscore",
["UI_TEXT_CONTENT.AISTUDIO.DIALOGS.AB.T1"] = "Without an underscore"
});
var withoutUnderscore = code.IndexOf("AISTUDIO::DIALOGS::AB::T1", StringComparison.Ordinal);
var withUnderscore = code.IndexOf("AISTUDIO::DIALOGS::A_B::T1", StringComparison.Ordinal);
using (Assert.EnterMultipleScope())
{
Assert.That(withoutUnderscore, Is.GreaterThanOrEqualTo(0), code);
Assert.That(withUnderscore, Is.GreaterThan(withoutUnderscore), code);
}
}
private static string Create(IReadOnlyDictionary<string, string> texts, IReadOnlyDictionary<string, string>? comments = null)
{
// The Lua runtime has a type of the same name, which stands for a table in Lua:
@@ -0,0 +1,98 @@
using System.Globalization;
using AIStudio.Tools.PluginSystem.Assistants;
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
namespace AIStudio.Tests.Tools.PluginSystem;
/// <summary>
/// Checks that assistant plugins read numbers and write dates the same way on every computer.
/// </summary>
/// <remarks>
/// The app sets no culture of its own, so the current culture is the one of the operating system. Each case runs
/// under a culture which writes differently than the invariant one: de-DE with a decimal comma and German names of
/// months, sv-SE with a minus sign of its own (U+2212), fi-FI with a period between hours and minutes, and th-TH
/// with the years of the Buddhist era. Each case first checks that its culture really writes differently, so that
/// a system without the data of the cultures fails instead of passing.
/// </remarks>
[TestFixture]
public sealed class AssistantPluginCultureTests
{
private static readonly DateTime MOMENT = new(2026, 3, 11, 14, 3, 22, 500);
[TestCase("de-DE", 12.5, "12.5")]
[TestCase("sv-SE", -3.5, "-3.5")]
public void ANumberFromLuaBecomesTheSameTextOnEveryComputer(string culture, double number, string expected)
{
InCulture(culture, () =>
{
var props = new Dictionary<string, object> { ["PrefillText"] = number };
using (Assert.EnterMultipleScope())
{
Assert.That(number.ToString(CultureInfo.CurrentCulture), Is.Not.EqualTo(expected), "The culture must write the number differently, or this case checks nothing.");
Assert.That(AssistantComponentPropHelper.ReadString(props, "PrefillText"), Is.EqualTo(expected));
}
});
}
[TestCase("de-DE")]
[TestCase("sv-SE")]
public void AWholeNumberIsReadOnEveryComputer(string culture)
{
InCulture(culture, () =>
{
var props = new Dictionary<string, object>
{
["FromLua"] = -3.0,
["FromASetter"] = 7,
["AsText"] = "-12",
["WithAFraction"] = 6.5,
["TooLarge"] = 1e15
};
using (Assert.EnterMultipleScope())
{
Assert.That(AssistantComponentPropHelper.ReadInt(props, "FromLua"), Is.EqualTo(-3));
Assert.That(AssistantComponentPropHelper.ReadInt(props, "FromASetter"), Is.EqualTo(7));
Assert.That(AssistantComponentPropHelper.ReadInt(props, "AsText"), Is.EqualTo(-12));
Assert.That(AssistantComponentPropHelper.ReadInt(props, "WithAFraction", fallback: 42), Is.EqualTo(42));
Assert.That(AssistantComponentPropHelper.ReadNullableInt(props, "TooLarge"), Is.Null);
Assert.That(AssistantComponentPropHelper.ReadNullableInt(props, "Missing"), Is.Null);
}
});
}
[TestCase("fi-FI", "yyyy-MM-dd HH:mm:ss", "2026-03-11 14:03:22")]
[TestCase("th-TH", "yyyy-MM-dd HH:mm:ss", "2026-03-11 14:03:22")]
[TestCase("de-DE", "dd. MMMM yyyy", "11. March 2026")]
public void TheDateTimeHelperFormatsTheSameOnEveryComputer(string culture, string format, string expected)
{
InCulture(culture, () =>
{
var table = PluginAssistants.CreateDateTimeTable(MOMENT, format);
using (Assert.EnterMultipleScope())
{
Assert.That(MOMENT.ToString(format, CultureInfo.CurrentCulture), Is.Not.EqualTo(expected), "The culture must write the moment differently, or this case checks nothing.");
Assert.That(table["formatted"].Read<string>(), Is.EqualTo(expected));
Assert.That(table["year"].Read<double>(), Is.EqualTo(2026));
Assert.That(table["millisecond"].Read<double>(), Is.EqualTo(500));
}
});
}
private static void InCulture(string name, Action check)
{
var previousCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(name);
check();
}
finally
{
CultureInfo.CurrentCulture = previousCulture;
}
}
}
@@ -24,9 +24,12 @@ namespace AIStudio.Tests.Tools.PluginSystem;
/// tables are compared value by value. A parser which read a single string differently than Lua
/// would hand the import something the configuration plugin of the same snippet never contains.<br/><br/>
/// Nothing here asserts on the wording of an issue: I18N is process-wide state without a reset, so
/// an Init somewhere else would decide whether these pass.
/// an Init somewhere else would decide whether these pass.<br/><br/>
/// All cases run under de-DE, which writes a decimal comma: an export which wrote a number with the
/// region settings of the computer would fail the comparison with Lua here.
/// </remarks>
[TestFixture]
[SetCulture("de-DE")]
public sealed class ConfigurationSnippetParserTests
{
/// <summary>
@@ -0,0 +1,39 @@
using System.Globalization;
using System.Text.Json.Nodes;
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.WebSearch.SearXNG;
namespace AIStudio.Tests.Tools.ToolCalling;
/// <summary>
/// Checks that the score of a SearXNG result is read the same way on every computer.
/// </summary>
/// <remarks>
/// The score decides the order of the results. An instance may send it as a number or as text, both in the notation
/// of JSON. All cases run under de-DE, where the period separates thousands: read with that culture, "0.75" was 75.
/// </remarks>
[TestFixture]
[SetCulture("de-DE")]
public sealed class SearXNGScoreTests
{
[Test]
public void TheCultureReadsThePeriodAsAThousandsSeparator()
{
Assert.That(double.Parse("0.75", CultureInfo.CurrentCulture), Is.EqualTo(75), "Without that, the other cases check nothing.");
}
[TestCase("""{ "score": 0.75 }""")]
[TestCase("""{ "score": "0.75" }""")]
public void TheScoreIsReadTheSameOnEveryComputer(string json)
{
var result = JsonNode.Parse(json)!.AsObject();
var found = SearXNGSearchClient.TryGetScore(result, out var score);
using (Assert.EnterMultipleScope())
{
Assert.That(found, Is.True);
Assert.That(score, Is.EqualTo(0.75));
}
}
}