mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 19:13:48 +00:00
Added detailed plugin information to the plugins page (#1013)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
b371b3d54f
commit
f912af321f
26 files changed
+884
-95
No files matched your search
@@ -98,6 +98,8 @@ public sealed class LeftOverConfigurationObjectsTests
|
||||
|
||||
public PluginVersion Version => new(1, 0, 0);
|
||||
|
||||
public DateTimeOffset? LastChanged => null;
|
||||
|
||||
public string[] Authors => [];
|
||||
|
||||
public string SupportContact => string.Empty;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
|
||||
namespace AIStudio.Tests.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Checks how a plugin states when it was last changed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// LAST_CHANGED is optional and always a point in time in UTC, so that the UI can show it in the time
|
||||
/// zone of the user. A date alone cannot be converted: whether a day in UTC is the same day locally
|
||||
/// depends on the time. A plugin without the field, or with a value of another form, still loads, and
|
||||
/// the value then counts as absent. AI Studio writes the field itself whenever it writes a direct chat
|
||||
/// launcher, and what it writes has to load back to the same point in time.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class PluginLastChangedTests
|
||||
{
|
||||
private static readonly Guid PLUGIN_ID = Guid.Parse("3f6a2c8e-9b1d-4e7a-8c5f-2d4b6e8a0c1f");
|
||||
|
||||
private static readonly DateTimeOffset LAST_CHANGED = new(2026, 10, 12, 14, 30, 5, TimeSpan.Zero);
|
||||
|
||||
private const string LAST_CHANGED_LINE = "LAST_CHANGED = \"2026-10-12T14:30:05Z\"";
|
||||
|
||||
[Test]
|
||||
public async Task APointInTimeInUtcIsRead()
|
||||
{
|
||||
using var plugin = await LoadModelPlugin(LAST_CHANGED_LINE);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
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]
|
||||
public async Task AMissingFieldIsNoIssue()
|
||||
{
|
||||
using var plugin = await LoadModelPlugin(string.Empty);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(plugin.IsValid, Is.True, string.Join(" ", plugin.Issues));
|
||||
Assert.That(plugin.LastChanged, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase("\"2026-10-12\"")]
|
||||
[TestCase("\"2026-10-12T14:30:05\"")]
|
||||
[TestCase("\"2026-10-12T16:30:05+02:00\"")]
|
||||
[TestCase("\"2026-13-01T00:00:00Z\"")]
|
||||
[TestCase("\"12.10.2026 14:30:05\"")]
|
||||
[TestCase("20261012")]
|
||||
public async Task AMalformedValueIsIgnoredAndThePluginStillLoads(string luaValue)
|
||||
{
|
||||
using var plugin = await LoadModelPlugin($"LAST_CHANGED = {luaValue}");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
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]
|
||||
public void ALocalTimeIsWrittenInUtc()
|
||||
{
|
||||
var localTime = new DateTimeOffset(2026, 10, 12, 16, 30, 5, TimeSpan.FromHours(2));
|
||||
|
||||
Assert.That(PluginBase.FormatLastChanged(localTime), Is.EqualTo("2026-10-12T14:30:05Z"), "The format ends in a literal Z, so a local time must not be written as it is.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TheWriterLeavesTheFieldOutWithoutAPointInTime()
|
||||
{
|
||||
var lua = DirectChatLauncherLuaWriter.Write(CreateLauncherMetadata(null), CreateLauncherDefinition());
|
||||
|
||||
Assert.That(lua, Does.Not.Contain("LAST_CHANGED"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task WhatTheWriterWritesLoadsBackToTheSamePointInTime()
|
||||
{
|
||||
var lua = DirectChatLauncherLuaWriter.Write(CreateLauncherMetadata(LAST_CHANGED), CreateLauncherDefinition());
|
||||
using var plugin = await PluginFactory.Load(null, lua);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
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>
|
||||
/// Loads a model plugin, the plugin type with the fewest fields beyond the ones every plugin has.
|
||||
/// </summary>
|
||||
/// <param name="lastChangedLine">The Lua line which states LAST_CHANGED, or an empty string.</param>
|
||||
private static Task<PluginBase> LoadModelPlugin(string lastChangedLine) => PluginFactory.Load(null, $$"""
|
||||
ID = "{{PLUGIN_ID}}"
|
||||
NAME = "Models for the tests"
|
||||
DESCRIPTION = "A model plugin which declares no models."
|
||||
VERSION = "1.0.0"
|
||||
{{lastChangedLine}}
|
||||
TYPE = "MODEL"
|
||||
AUTHORS = {"MindWork AI"}
|
||||
SUPPORT_CONTACT = "MindWork AI"
|
||||
SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
|
||||
CATEGORIES = {"CORE"}
|
||||
TARGET_GROUPS = {"EVERYONE"}
|
||||
IS_MAINTAINED = true
|
||||
DEPRECATION_MESSAGE = ""
|
||||
MODELS = {}
|
||||
""");
|
||||
|
||||
private static DirectChatLauncherPluginMetadata CreateLauncherMetadata(DateTimeOffset? lastChanged) => new(PLUGIN_ID, "1.0.0", lastChanged, ["MindWork AI"], "MindWork AI",
|
||||
"https://github.com/MindWorkAI/AI-Studio", [PluginCategory.CORE], [PluginTargetGroup.EVERYONE], IsMaintained: true, DeprecationMessage: string.Empty, IsAssistantBuilderGenerated: false);
|
||||
|
||||
private static DirectChatLauncherDefinition CreateLauncherDefinition() => new("Launcher for the tests", "Launcher for the tests", "Opens a disappearing chat.", new AssistantChatLaunchConfiguration(string.Empty, null, null, null, null, null));
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tests.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Checks which texts in the metadata of a plugin count as links.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AI Studio offers such a link as a button, and a click opens it right away. A plugin may therefore
|
||||
/// only link to a website or to a mail address. Any other scheme would let a plugin open something on
|
||||
/// the computer of the user, and plain text, which a support contact may be, gets no button at all. The
|
||||
/// links which the Assistant Builder fills in tell nothing about a plugin and count as not stated.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class PluginLinksTests
|
||||
{
|
||||
[TestCase("https://example.org")]
|
||||
[TestCase("http://example.org/plugins")]
|
||||
[TestCase("mailto:support@example.org")]
|
||||
[TestCase("mailto:support@example.org?subject=Plugin")]
|
||||
[TestCase(" https://example.org ")]
|
||||
public void AWebsiteOrAMailAddressIsALink(string text)
|
||||
{
|
||||
Assert.That(PluginLinks.TryParse(text, out _), Is.True);
|
||||
}
|
||||
|
||||
[TestCase("")]
|
||||
[TestCase("MindWork AI")]
|
||||
[TestCase("example.org")]
|
||||
[TestCase("file:///etc/passwd")]
|
||||
[TestCase("javascript:alert(1)")]
|
||||
[TestCase("ftp://example.org")]
|
||||
[TestCase("mailto:")]
|
||||
[TestCase("mailto:?subject=Plugin")]
|
||||
public void AnythingElseIsNoLink(string text)
|
||||
{
|
||||
Assert.That(PluginLinks.TryParse(text, out _), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnlyAMailtoLinkWritesAMail()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
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)]
|
||||
[TestCase(AssistantPluginGenerationService.DEFAULT_SOURCE_URL, true)]
|
||||
[TestCase($" {AssistantPluginGenerationService.DEFAULT_SOURCE_URL} ", true)]
|
||||
[TestCase("mailto:support@example.org", false)]
|
||||
[TestCase("https://github.com/MindWorkAI/AI-Studio/wiki", false)]
|
||||
public void OnlyTheLinksOfTheAssistantBuilderArePlaceholders(string text, bool isPlaceholder)
|
||||
{
|
||||
Assert.That(PluginLinks.IsPlaceholder(text), Is.EqualTo(isPlaceholder));
|
||||
}
|
||||
|
||||
[TestCase("mailto:support@example.org", "support@example.org")]
|
||||
[TestCase("mailto:support@example.org?subject=Plugin", "support@example.org")]
|
||||
[TestCase("mailto:support%2Bplugins@example.org", "support+plugins@example.org")]
|
||||
[TestCase("https://example.org/plugins?id=1", "https://example.org/plugins?id=1")]
|
||||
public void AMailLinkShowsItsAddressAndAWebsiteShowsItself(string text, string displayText)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(PluginLinks.TryParse(text, out var link), Is.True);
|
||||
Assert.That(PluginLinks.GetDisplayText(link!), Is.EqualTo(displayText));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user