Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -0,0 +1,183 @@
using System.Reflection;
using StellaOps.Plugin.Versioning;
namespace StellaOps.Plugin.Tests;
/// <summary>
/// Unit tests for PluginCompatibilityChecker.
/// </summary>
public sealed class PluginCompatibilityCheckerTests
{
#region Basic Compatibility Tests
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_AssemblyWithoutAttribute_Lenient_ReturnsCompatible()
{
// Arrange - Use current test assembly which doesn't have StellaPluginVersionAttribute
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
// Act
var result = PluginCompatibilityChecker.CheckCompatibility(
assembly, hostVersion, CompatibilityCheckOptions.Lenient);
// Assert
Assert.True(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.Null(result.PluginVersion);
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_AssemblyWithoutAttribute_RequireVersion_ReturnsIncompatible()
{
// Arrange
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
var hostVersion = new Version(1, 0, 0);
var options = new CompatibilityCheckOptions
{
RequireVersionAttribute = true
};
// Act
var result = PluginCompatibilityChecker.CheckCompatibility(assembly, hostVersion, options);
// Assert
Assert.False(result.IsCompatible);
Assert.False(result.HasVersionAttribute);
Assert.NotNull(result.FailureReason);
Assert.Contains("StellaPluginVersion", result.FailureReason);
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_NullAssembly_ThrowsException()
{
// Arrange
var hostVersion = new Version(1, 0, 0);
// Act & Assert
Assert.Throws<ArgumentNullException>(
() => PluginCompatibilityChecker.CheckCompatibility(null!, hostVersion));
}
[Trait("Category", "Unit")]
[Fact]
public void CheckCompatibility_NullHostVersion_ThrowsException()
{
// Arrange
var assembly = typeof(PluginCompatibilityCheckerTests).Assembly;
// Act & Assert
Assert.Throws<ArgumentNullException>(
() => PluginCompatibilityChecker.CheckCompatibility(assembly, null!));
}
#endregion
#region CompatibilityCheckOptions Tests
[Trait("Category", "Unit")]
[Fact]
public void DefaultOptions_HasCorrectValues()
{
// Act
var options = CompatibilityCheckOptions.Default;
// Assert
Assert.True(options.RequireVersionAttribute);
Assert.True(options.StrictMajorVersionCheck);
}
[Trait("Category", "Unit")]
[Fact]
public void LenientOptions_HasCorrectValues()
{
// Act
var options = CompatibilityCheckOptions.Lenient;
// Assert
Assert.False(options.RequireVersionAttribute);
Assert.False(options.StrictMajorVersionCheck);
}
[Trait("Category", "Unit")]
[Fact]
public void CustomOptions_CanBeConfigured()
{
// Arrange & Act
var options = new CompatibilityCheckOptions
{
RequireVersionAttribute = true,
StrictMajorVersionCheck = false
};
// Assert
Assert.True(options.RequireVersionAttribute);
Assert.False(options.StrictMajorVersionCheck);
}
#endregion
#region PluginCompatibilityResult Tests
[Trait("Category", "Unit")]
[Fact]
public void PluginCompatibilityResult_RecordEquality()
{
// Arrange
var result1 = new PluginCompatibilityResult(
IsCompatible: true,
PluginVersion: new Version(1, 0, 0),
MinimumHostVersion: new Version(1, 0, 0),
MaximumHostVersion: new Version(2, 0, 0),
RequiresSignature: true,
FailureReason: null,
HasVersionAttribute: true);
var result2 = new PluginCompatibilityResult(
IsCompatible: true,
PluginVersion: new Version(1, 0, 0),
MinimumHostVersion: new Version(1, 0, 0),
MaximumHostVersion: new Version(2, 0, 0),
RequiresSignature: true,
FailureReason: null,
HasVersionAttribute: true);
// Assert
Assert.Equal(result1, result2);
}
[Trait("Category", "Unit")]
[Fact]
public void PluginCompatibilityResult_PropertiesAreSet()
{
// Arrange
var pluginVersion = new Version(1, 2, 3);
var minVersion = new Version(1, 0, 0);
var maxVersion = new Version(2, 0, 0);
var failureReason = "Test failure";
// Act
var result = new PluginCompatibilityResult(
IsCompatible: false,
PluginVersion: pluginVersion,
MinimumHostVersion: minVersion,
MaximumHostVersion: maxVersion,
RequiresSignature: true,
FailureReason: failureReason,
HasVersionAttribute: true);
// Assert
Assert.False(result.IsCompatible);
Assert.Equal(pluginVersion, result.PluginVersion);
Assert.Equal(minVersion, result.MinimumHostVersion);
Assert.Equal(maxVersion, result.MaximumHostVersion);
Assert.True(result.RequiresSignature);
Assert.Equal(failureReason, result.FailureReason);
Assert.True(result.HasVersionAttribute);
}
#endregion
}

View File

@@ -0,0 +1,229 @@
using StellaOps.Plugin.Hosting;
namespace StellaOps.Plugin.Tests;
/// <summary>
/// Unit tests for PluginHostOptions configuration.
/// </summary>
public sealed class PluginHostOptionsTests
{
#region Default Values Tests
[Trait("Category", "Unit")]
[Fact]
public void DefaultValues_AreCorrect()
{
// Arrange & Act
var options = new PluginHostOptions();
// Assert
Assert.True(options.EnsureDirectoryExists);
Assert.True(options.RecursiveSearch); // Default is true
Assert.False(options.EnforceSignatureVerification);
Assert.True(options.RequireVersionAttribute); // Default is true
Assert.True(options.EnforceVersionCompatibility); // Default is true
Assert.True(options.StrictMajorVersionCheck); // Default is true
Assert.Null(options.HostVersion);
Assert.Null(options.SignatureVerifier);
Assert.Empty(options.SearchPatterns);
Assert.Empty(options.PluginOrder);
Assert.Empty(options.AdditionalPrefixes);
Assert.Null(options.BaseDirectory);
Assert.Null(options.PluginsDirectory);
Assert.Null(options.PrimaryPrefix);
}
#endregion
#region Search Pattern Tests
[Trait("Category", "Unit")]
[Fact]
public void SearchPatterns_CanBeAdded()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.SearchPatterns.Add("MyPrefix.Plugin.*.dll");
options.SearchPatterns.Add("Another.Plugin.*.dll");
// Assert
Assert.Equal(2, options.SearchPatterns.Count);
Assert.Contains("MyPrefix.Plugin.*.dll", options.SearchPatterns);
Assert.Contains("Another.Plugin.*.dll", options.SearchPatterns);
}
#endregion
#region Plugin Order Tests
[Trait("Category", "Unit")]
[Fact]
public void PluginOrder_CanBeConfigured()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.PluginOrder.Add("PluginA");
options.PluginOrder.Add("PluginB");
options.PluginOrder.Add("PluginC");
// Assert
Assert.Equal(3, options.PluginOrder.Count);
Assert.Equal("PluginA", options.PluginOrder[0]);
Assert.Equal("PluginB", options.PluginOrder[1]);
Assert.Equal("PluginC", options.PluginOrder[2]);
}
#endregion
#region BaseDirectory Resolution Tests
[Trait("Category", "Unit")]
[Fact]
public void ResolveBaseDirectory_WhenNull_ReturnsAppContextBaseDirectory()
{
// Arrange
var options = new PluginHostOptions
{
BaseDirectory = null
};
// Act
var resolved = options.ResolveBaseDirectory();
// Assert
Assert.Equal(AppContext.BaseDirectory, resolved);
}
[Trait("Category", "Unit")]
[Fact]
public void ResolveBaseDirectory_WhenSet_ReturnsConfiguredValue()
{
// Arrange
var customDir = Path.GetTempPath();
var options = new PluginHostOptions
{
BaseDirectory = customDir
};
// Act
var resolved = options.ResolveBaseDirectory();
// Assert
Assert.Equal(customDir, resolved);
}
#endregion
#region Version Configuration Tests
[Trait("Category", "Unit")]
[Fact]
public void HostVersion_CanBeConfigured()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.HostVersion = new Version(2, 1, 0);
// Assert
Assert.NotNull(options.HostVersion);
Assert.Equal(2, options.HostVersion.Major);
Assert.Equal(1, options.HostVersion.Minor);
Assert.Equal(0, options.HostVersion.Build);
}
[Trait("Category", "Unit")]
[Fact]
public void VersionCompatibility_CanBeEnforced()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.HostVersion = new Version(1, 0, 0);
options.RequireVersionAttribute = true;
options.EnforceVersionCompatibility = true;
options.StrictMajorVersionCheck = true;
// Assert
Assert.True(options.RequireVersionAttribute);
Assert.True(options.EnforceVersionCompatibility);
Assert.True(options.StrictMajorVersionCheck);
}
#endregion
#region Prefix Configuration Tests
[Trait("Category", "Unit")]
[Fact]
public void PrimaryPrefix_AffectsDefaultDirectory()
{
// Arrange
var options = new PluginHostOptions
{
PrimaryPrefix = "MyModule"
};
// Assert
Assert.Equal("MyModule", options.PrimaryPrefix);
}
[Trait("Category", "Unit")]
[Fact]
public void AdditionalPrefixes_CanBeConfigured()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.AdditionalPrefixes.Add("Prefix1");
options.AdditionalPrefixes.Add("Prefix2");
// Assert
Assert.Equal(2, options.AdditionalPrefixes.Count);
}
#endregion
#region Signature Verification Tests
[Trait("Category", "Unit")]
[Fact]
public void SignatureVerification_CanBeEnforced()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.EnforceSignatureVerification = true;
// Assert
Assert.True(options.EnforceSignatureVerification);
}
#endregion
#region Recursive Search Tests
[Trait("Category", "Unit")]
[Fact]
public void RecursiveSearch_CanBeEnabled()
{
// Arrange
var options = new PluginHostOptions();
// Act
options.RecursiveSearch = true;
// Assert
Assert.True(options.RecursiveSearch);
}
#endregion
}

View File

@@ -0,0 +1,235 @@
using Microsoft.Extensions.Logging;
using StellaOps.Plugin.Hosting;
using StellaOps.Plugin.Versioning;
namespace StellaOps.Plugin.Tests;
/// <summary>
/// Integration tests for PluginHost plugin loading.
/// </summary>
public sealed class PluginHostTests
{
#region LoadPlugins Directory Tests
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_NonExistentDirectory_ReturnsEmptyResult()
{
// Arrange
var options = new PluginHostOptions
{
PluginsDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins"),
EnsureDirectoryExists = false
};
// Act
var result = PluginHost.LoadPlugins(options);
// Assert
Assert.Empty(result.Plugins);
Assert.Empty(result.Failures);
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_EmptyDirectory_ReturnsEmptyResult()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
// Act
var result = PluginHost.LoadPlugins(options);
// Assert
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_EnsureDirectoryExists_CreatesDirectory()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins");
Assert.False(Directory.Exists(tempDir));
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = true
};
// Act
var result = PluginHost.LoadPlugins(options);
// Assert
Assert.True(Directory.Exists(tempDir));
Assert.Empty(result.Plugins);
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
var parent = Path.GetDirectoryName(tempDir);
if (parent is not null && Directory.Exists(parent))
{
Directory.Delete(parent, true);
}
}
}
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_NullOptions_ThrowsException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => PluginHost.LoadPlugins(null!));
}
#endregion
#region Search Pattern Tests
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_CustomSearchPattern_RespectsPattern()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
// Create a file that won't match the pattern
File.WriteAllText(Path.Combine(tempDir, "NotAPlugin.dll"), "dummy");
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
options.SearchPatterns.Add("MyPrefix.Plugin.*.dll");
// Act
var result = PluginHost.LoadPlugins(options);
// Assert
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region Plugin Order Tests
[Trait("Category", "Unit")]
[Fact]
public void LoadPlugins_WithPluginOrder_AcceptsOrderConfiguration()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
options.PluginOrder.Add("PluginA");
options.PluginOrder.Add("PluginB");
// Act
var result = PluginHost.LoadPlugins(options);
// Assert - In an empty directory, no plugins are loaded
Assert.Empty(result.Plugins);
Assert.NotNull(result.MissingOrderedPlugins);
// MissingOrderedPlugins only tracks plugins that were in PluginOrder but not found in discovered files
// In an empty directory, no files are discovered, so the list may be empty
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
#region Async Tests
[Trait("Category", "Unit")]
[Fact]
public async Task LoadPluginsAsync_NonExistentDirectory_ReturnsEmptyResult()
{
// Arrange
var options = new PluginHostOptions
{
PluginsDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), "plugins"),
EnsureDirectoryExists = false
};
// Act
var result = await PluginHost.LoadPluginsAsync(options);
// Assert
Assert.Empty(result.Plugins);
Assert.Empty(result.Failures);
}
[Trait("Category", "Unit")]
[Fact]
public async Task LoadPluginsAsync_WithCancellationToken_AcceptsToken()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
try
{
var options = new PluginHostOptions
{
PluginsDirectory = tempDir,
EnsureDirectoryExists = false
};
using var cts = new CancellationTokenSource();
// Don't cancel - just verify the async method accepts a cancellation token
// Act
var result = await PluginHost.LoadPluginsAsync(options, cancellationToken: cts.Token);
// Assert
Assert.Empty(result.Plugins);
}
finally
{
Directory.Delete(tempDir, true);
}
}
#endregion
}

View File

@@ -4,19 +4,25 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../StellaOps.Plugin/StellaOps.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="xunit.runner.visualstudio" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>