chore(sprints): archive 20260226 advisories and expand deterministic tests

This commit is contained in:
master
2026-03-04 03:09:23 +02:00
parent 4fe8eb56ae
commit aaad8104cb
35 changed files with 4686 additions and 1 deletions

View File

@@ -44,4 +44,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| SPRINT_20260224_004-LOC-306 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added dedicated `/settings/language` UX wiring that reuses Platform persisted language preference API for authenticated users. |
| SPRINT_20260224_004-LOC-307 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added Ukrainian locale support (`uk-UA`) across Platform translation assets and preference normalization aliases (`uk-UA`/`uk_UA`/`uk`/`ua`). |
| SPRINT_20260224_004-LOC-308 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: platform locale catalog endpoint (`GET /api/v1/platform/localization/locales`) is now consumed by both UI and CLI locale-selection paths. |
| SPRINT_20260226_230-LOC-001 | DONE | Sprint `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_230_Platform_locale_label_translation_corrections.md`: completed non-English translation correction across Platform/Web/shared localization bundles (`bg-BG`, `de-DE`, `es-ES`, `fr-FR`, `ru-RU`, `uk-UA`, `zh-CN`, `zh-TW`), including cleanup of placeholder/transliteration/malformed values (`Ezik`, leaked token markers, mojibake) and a context-quality pass for backend German resource bundles (`graph`, `policy`, `scanner`, `advisoryai`). |
| PLATFORM-223-001 | DONE | Sprint `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md`: shipped deterministic score explain/replay contract completion (`unknowns`, `proof_ref`, deterministic replay envelope parsing/verification differences) and updated score API/module docs with contract notes. |

View File

@@ -0,0 +1,297 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.Platform.WebService.Services;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class ScoreExplainEndpointContractTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public ScoreExplainEndpointContractTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Explain_WhenDigestExists_ReturnsDeterministicContract()
{
using var deterministicFactory = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IScoreEvaluationService>();
services.AddSingleton<IScoreEvaluationService, StaticScoreEvaluationService>();
});
});
using var client = CreateClient(deterministicFactory, "tenant-score-explain-valid");
const string digest = "sha256:abc123";
var explainResponseA = await client.GetAsync(
"/api/v1/score/explain/SHA256:ABC123",
TestContext.Current.CancellationToken);
explainResponseA.EnsureSuccessStatusCode();
using var explainA = JsonDocument.Parse(
await explainResponseA.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
var itemA = GetAnyProperty(explainA.RootElement, "item", "Item");
Assert.Equal("score.explain.v1", GetAnyProperty(itemA, "contract_version", "contractVersion", "ContractVersion").GetString());
Assert.Equal(digest, GetAnyProperty(itemA, "digest", "Digest").GetString());
Assert.Equal(digest.ToLowerInvariant(), GetAnyProperty(itemA, "deterministic_input_hash", "deterministicInputHash", "DeterministicInputHash").GetString());
Assert.True(GetAnyProperty(itemA, "factors", "Factors").GetArrayLength() > 0);
Assert.True(GetAnyProperty(itemA, "sources", "Sources").GetArrayLength() > 0);
var explainResponseB = await client.GetAsync(
$"/api/v1/score/explain/{Uri.EscapeDataString(digest)}",
TestContext.Current.CancellationToken);
explainResponseB.EnsureSuccessStatusCode();
using var explainB = JsonDocument.Parse(
await explainResponseB.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
var itemB = GetAnyProperty(explainB.RootElement, "item", "Item");
var itemAJson = JsonSerializer.Serialize(itemA);
var itemBJson = JsonSerializer.Serialize(itemB);
Assert.Equal(itemAJson, itemBJson);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Explain_WhenDigestOmitsAlgorithm_NormalizesToSha256Deterministically()
{
using var deterministicFactory = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IScoreEvaluationService>();
services.AddSingleton<IScoreEvaluationService, StaticScoreEvaluationService>();
});
});
using var client = CreateClient(deterministicFactory, "tenant-score-explain-normalized");
var response = await client.GetAsync(
"/api/v1/score/explain/ABC123",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
var item = GetAnyProperty(payload.RootElement, "item", "Item");
Assert.Equal("sha256:abc123", GetAnyProperty(item, "digest", "Digest").GetString());
Assert.Equal("sha256:abc123", GetAnyProperty(item, "deterministic_input_hash", "deterministicInputHash", "DeterministicInputHash").GetString());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Explain_WhenDigestMissing_ReturnsDeterministicNotFound()
{
using var client = CreateClient(_factory, "tenant-score-explain-not-found");
var response = await client.GetAsync(
"/api/v1/score/explain/sha256:does-not-exist",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
var error = await response.Content.ReadFromJsonAsync<ScoreExplainErrorResponse>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(error);
Assert.Equal("not_found", error!.Code);
Assert.Equal("sha256:does-not-exist", error.Digest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Explain_WhenDigestInvalid_ReturnsDeterministicInvalidInput()
{
using var client = CreateClient(_factory, "tenant-score-explain-invalid");
var response = await client.GetAsync(
"/api/v1/score/explain/%20",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var error = await response.Content.ReadFromJsonAsync<ScoreExplainErrorResponse>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(error);
Assert.Equal("invalid_input", error!.Code);
Assert.Equal(" ", error.Digest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Explain_WhenDigestHasMissingHashSegment_ReturnsDeterministicInvalidInput()
{
using var client = CreateClient(_factory, "tenant-score-explain-invalid-segment");
var response = await client.GetAsync(
$"/api/v1/score/explain/{Uri.EscapeDataString("sha256:")}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var error = await response.Content.ReadFromJsonAsync<ScoreExplainErrorResponse>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(error);
Assert.Equal("invalid_input", error!.Code);
Assert.Equal("sha256:", error.Digest);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Explain_WhenBackendUnavailable_ReturnsDeterministicBackendUnavailable()
{
using var backendUnavailableFactory = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IScoreEvaluationService>();
services.AddSingleton<IScoreEvaluationService, ThrowingScoreEvaluationService>();
});
});
using var client = CreateClient(backendUnavailableFactory, "tenant-score-explain-backend");
var response = await client.GetAsync(
"/api/v1/score/explain/sha256:abc123",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
var error = await response.Content.ReadFromJsonAsync<ScoreExplainErrorResponse>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(error);
Assert.Equal("backend_unavailable", error!.Code);
Assert.Equal("sha256:abc123", error.Digest);
}
private static HttpClient CreateClient(WebApplicationFactory<StellaOps.Platform.WebService.Options.PlatformServiceOptions> factory, string tenantId)
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "score-test-actor");
return client;
}
private sealed class ThrowingScoreEvaluationService : IScoreEvaluationService
{
public Task<PlatformCacheResult<ScoreEvaluateResponse>> EvaluateAsync(PlatformRequestContext context, ScoreEvaluateRequest request, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<ScoreEvaluateResponse?>> GetByIdAsync(PlatformRequestContext context, string scoreId, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<ScoreExplainResponse?>> GetExplanationAsync(PlatformRequestContext context, string digest, CancellationToken ct = default) =>
throw new InvalidOperationException("backend unavailable");
public Task<PlatformCacheResult<IReadOnlyList<WeightManifestSummary>>> ListWeightManifestsAsync(PlatformRequestContext context, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<WeightManifestDetail?>> GetWeightManifestAsync(PlatformRequestContext context, string version, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<WeightManifestDetail?>> GetEffectiveWeightManifestAsync(PlatformRequestContext context, DateTimeOffset asOf, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<IReadOnlyList<ScoreHistoryRecord>>> GetHistoryAsync(PlatformRequestContext context, string cveId, string? purl, int limit, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<ScoreReplayResponse?>> GetReplayAsync(PlatformRequestContext context, string scoreId, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<ScoreVerifyResponse>> VerifyReplayAsync(PlatformRequestContext context, ScoreVerifyRequest request, CancellationToken ct = default) =>
throw new NotSupportedException();
}
private sealed class StaticScoreEvaluationService : IScoreEvaluationService
{
private static readonly ScoreExplainResponse Response = new()
{
ContractVersion = "score.explain.v1",
Digest = "sha256:abc123",
ScoreId = "score_abc123",
FinalScore = 62,
Bucket = "Investigate",
ComputedAt = DateTimeOffset.Parse("2026-02-26T12:00:00Z"),
DeterministicInputHash = "sha256:abc123",
ReplayLink = "/api/v1/score/score_abc123/replay",
Factors =
[
new ScoreExplainFactor
{
Name = "reachability",
Weight = 0.25,
Value = 1.0,
Contribution = 0.25
}
],
Sources =
[
new ScoreExplainSource
{
SourceType = "score_history",
SourceRef = "score-history:score_abc123",
SourceDigest = "sha256:abc123"
}
]
};
public Task<PlatformCacheResult<ScoreEvaluateResponse>> EvaluateAsync(PlatformRequestContext context, ScoreEvaluateRequest request, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<ScoreEvaluateResponse?>> GetByIdAsync(PlatformRequestContext context, string scoreId, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<ScoreExplainResponse?>> GetExplanationAsync(PlatformRequestContext context, string digest, CancellationToken ct = default)
{
var normalized = digest.ToLowerInvariant();
var value = string.Equals(normalized, "sha256:abc123", StringComparison.Ordinal)
? Response
: null;
return Task.FromResult(new PlatformCacheResult<ScoreExplainResponse?>(
value,
DateTimeOffset.Parse("2026-02-26T12:00:00Z"),
Cached: true,
CacheTtlSeconds: 300));
}
public Task<PlatformCacheResult<IReadOnlyList<WeightManifestSummary>>> ListWeightManifestsAsync(PlatformRequestContext context, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<WeightManifestDetail?>> GetWeightManifestAsync(PlatformRequestContext context, string version, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<WeightManifestDetail?>> GetEffectiveWeightManifestAsync(PlatformRequestContext context, DateTimeOffset asOf, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<IReadOnlyList<ScoreHistoryRecord>>> GetHistoryAsync(PlatformRequestContext context, string cveId, string? purl, int limit, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<ScoreReplayResponse?>> GetReplayAsync(PlatformRequestContext context, string scoreId, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<PlatformCacheResult<ScoreVerifyResponse>> VerifyReplayAsync(PlatformRequestContext context, ScoreVerifyRequest request, CancellationToken ct = default) =>
throw new NotSupportedException();
}
private static JsonElement GetAnyProperty(JsonElement element, params string[] names)
{
foreach (var name in names)
{
if (element.TryGetProperty(name, out var value))
{
return value;
}
}
throw new KeyNotFoundException($"None of the expected properties [{string.Join(", ", names)}] were found.");
}
}