save progress

This commit is contained in:
StellaOps Bot
2026-01-03 00:47:24 +02:00
parent 3f197814c5
commit ca578801fd
319 changed files with 32478 additions and 2202 deletions

View File

@@ -0,0 +1,172 @@
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Inference.LlmProviders;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.AdvisoryAI.Tests;
public class LlmInferenceCacheTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CacheKey_UsesInvariantCulture()
{
var originalCulture = CultureInfo.CurrentCulture;
var originalUiCulture = CultureInfo.CurrentUICulture;
try
{
var options = Options.Create(new LlmInferenceCacheOptions
{
DeterministicOnly = false,
DefaultTtl = TimeSpan.FromMinutes(5),
ShortTtl = TimeSpan.FromMinutes(5)
});
var cache = new InMemoryLlmInferenceCache(options, NullLogger<InMemoryLlmInferenceCache>.Instance, new FakeTimeProvider());
var request = new LlmCompletionRequest
{
UserPrompt = "hello",
Temperature = 0.1,
MaxTokens = 10,
Model = "model-x"
};
var result = new LlmCompletionResult
{
Content = "ok",
ModelId = "model-x",
ProviderId = "openai",
Deterministic = false
};
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
CultureInfo.CurrentUICulture = new CultureInfo("de-DE");
await cache.SetAsync(request, "openai", result, CancellationToken.None);
CultureInfo.CurrentCulture = new CultureInfo("en-US");
CultureInfo.CurrentUICulture = new CultureInfo("en-US");
var cached = await cache.TryGetAsync(request, "openai", CancellationToken.None);
Assert.NotNull(cached);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUiCulture;
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SlidingExpiration_ExtendsExpiry()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 12, 30, 12, 0, 0, TimeSpan.Zero));
var options = Options.Create(new LlmInferenceCacheOptions
{
DeterministicOnly = false,
SlidingExpiration = true,
DefaultTtl = TimeSpan.FromMinutes(10),
MaxTtl = TimeSpan.FromMinutes(30)
});
var cache = new InMemoryLlmInferenceCache(options, NullLogger<InMemoryLlmInferenceCache>.Instance, timeProvider);
var request = new LlmCompletionRequest
{
UserPrompt = "hello",
Temperature = 0.0,
MaxTokens = 10,
Model = "model-x"
};
var result = new LlmCompletionResult
{
Content = "ok",
ModelId = "model-x",
ProviderId = "openai",
Deterministic = true
};
await cache.SetAsync(request, "openai", result, CancellationToken.None);
timeProvider.Advance(TimeSpan.FromMinutes(9));
var first = await cache.TryGetAsync(request, "openai", CancellationToken.None);
Assert.NotNull(first);
timeProvider.Advance(TimeSpan.FromMinutes(6));
var second = await cache.TryGetAsync(request, "openai", CancellationToken.None);
Assert.NotNull(second);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task MaxEntries_EvictsOldestEntries()
{
var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 12, 30, 13, 0, 0, TimeSpan.Zero));
var options = Options.Create(new LlmInferenceCacheOptions
{
DeterministicOnly = false,
MaxEntries = 1,
DefaultTtl = TimeSpan.FromMinutes(10),
ShortTtl = TimeSpan.FromMinutes(10)
});
var cache = new InMemoryLlmInferenceCache(options, NullLogger<InMemoryLlmInferenceCache>.Instance, timeProvider);
var request1 = new LlmCompletionRequest
{
UserPrompt = "hello",
Temperature = 0.0,
MaxTokens = 10,
Model = "model-x"
};
var request2 = new LlmCompletionRequest
{
UserPrompt = "world",
Temperature = 0.0,
MaxTokens = 10,
Model = "model-x"
};
var result = new LlmCompletionResult
{
Content = "ok",
ModelId = "model-x",
ProviderId = "openai",
Deterministic = true
};
await cache.SetAsync(request1, "openai", result, CancellationToken.None);
timeProvider.Advance(TimeSpan.FromSeconds(1));
await cache.SetAsync(request2, "openai", result, CancellationToken.None);
var evicted = await cache.TryGetAsync(request1, "openai", CancellationToken.None);
var retained = await cache.TryGetAsync(request2, "openai", CancellationToken.None);
Assert.Null(evicted);
Assert.NotNull(retained);
}
private sealed class FakeTimeProvider : TimeProvider
{
private DateTimeOffset current;
public FakeTimeProvider()
: this(DateTimeOffset.UtcNow)
{
}
public FakeTimeProvider(DateTimeOffset start)
{
current = start;
}
public void Advance(TimeSpan delta)
{
current = current.Add(delta);
}
public override DateTimeOffset GetUtcNow() => current;
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using StellaOps.AdvisoryAI.Inference.LlmProviders;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.AdvisoryAI.Tests;
public class LlmProviderConfigValidationTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void OpenAiConfigValidation_FailsWithoutApiKey()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["enabled"] = "true",
["api:baseUrl"] = "https://api.openai.com/v1"
})
.Build();
var plugin = new OpenAiLlmProviderPlugin();
var validation = plugin.ValidateConfiguration(configuration);
Assert.False(validation.IsValid);
Assert.Contains(validation.Errors, error => error.Contains("API key", StringComparison.OrdinalIgnoreCase));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void OpenAiConfigValidation_WarnsWhenDisabled()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["enabled"] = "false"
})
.Build();
var plugin = new OpenAiLlmProviderPlugin();
var validation = plugin.ValidateConfiguration(configuration);
Assert.True(validation.IsValid);
Assert.Contains(validation.Warnings, warning => warning.Contains("disabled", StringComparison.OrdinalIgnoreCase));
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.AdvisoryAI.Inference;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.AdvisoryAI.Tests;
public class SignedModelBundleManagerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SignBundleAsync_UsesDeterministicTimestamp()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-ai", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
try
{
var manifestPath = Path.Combine(tempRoot, "manifest.json");
await File.WriteAllTextAsync(manifestPath, CreateManifestJson(), CancellationToken.None);
var fixedTime = new DateTimeOffset(2025, 12, 31, 12, 34, 56, TimeSpan.Zero);
var manager = new SignedModelBundleManager(new FakeTimeProvider(fixedTime));
var signer = new FakeSigner("key-1", "ed25519");
var result = await manager.SignBundleAsync(tempRoot, signer, CancellationToken.None);
Assert.True(result.Success);
Assert.StartsWith("ed25519-20251231123456-", result.SignatureId, StringComparison.Ordinal);
var envelopePath = Path.Combine(tempRoot, "signature.dsse");
var envelopeJson = await File.ReadAllTextAsync(envelopePath, CancellationToken.None);
var envelope = JsonSerializer.Deserialize<ModelBundleSignatureEnvelope>(envelopeJson);
Assert.NotNull(envelope);
var payloadJson = Encoding.UTF8.GetString(Convert.FromBase64String(envelope!.Payload));
using var document = JsonDocument.Parse(payloadJson);
var signedAt = document.RootElement.GetProperty("signed_at").GetString();
Assert.Equal("2025-12-31T12:34:56.0000000+00:00", signedAt);
var manifestJson = await File.ReadAllTextAsync(manifestPath, CancellationToken.None);
using var manifestDoc = JsonDocument.Parse(manifestJson);
Assert.Equal(result.SignatureId, manifestDoc.RootElement.GetProperty("signature_id").GetString());
Assert.Equal("ed25519", manifestDoc.RootElement.GetProperty("crypto_scheme").GetString());
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
private static string CreateManifestJson()
{
return """
{
"version": "1.0.0",
"name": "test-model",
"description": "fixture",
"license": "MIT",
"size_category": "small",
"quantizations": ["q4"],
"files": [
{ "path": "model.bin", "digest": "abc", "size": 1, "type": "model" }
],
"created_at": "2025-12-01T00:00:00Z"
}
""";
}
private sealed class FakeSigner : IModelBundleSigner
{
public FakeSigner(string keyId, string scheme)
{
KeyId = keyId;
CryptoScheme = scheme;
}
public string KeyId { get; }
public string CryptoScheme { get; }
public Task<byte[]> SignAsync(byte[] data, CancellationToken cancellationToken = default)
=> Task.FromResult(Encoding.UTF8.GetBytes("sig"));
}
private sealed class FakeTimeProvider : TimeProvider
{
private readonly DateTimeOffset fixedNow;
public FakeTimeProvider(DateTimeOffset fixedNow) => this.fixedNow = fixedNow;
public override DateTimeOffset GetUtcNow() => fixedNow;
}
}