stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -0,0 +1,32 @@
# StellaOps.Provcache.Valkey.Tests - Local Agent Charter
## Roles
- Backend developer
- QA automation engineer
## Working directory
- src/__Libraries/__Tests/StellaOps.Provcache.Valkey.Tests
## Allowed dependencies
- src/__Libraries/StellaOps.Provcache.Valkey
- src/__Libraries/StellaOps.Provcache
- src/__Libraries/StellaOps.TestKit
## Required reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- docs/modules/prov-cache/README.md
- docs/modules/prov-cache/architecture.md
- docs/modules/prov-cache/metrics-alerting.md
## Determinism and test rules
- Use deterministic inputs: avoid DateTime.UtcNow, DateTimeOffset.UtcNow, Guid.NewGuid, and Random.Shared in tests.
- Use TimeProvider and fixed seeds or fixtures for time- and randomness-dependent tests.
- Use CultureInfo.InvariantCulture for parsing and formatting in tests.
- Tag tests with TestCategories (Unit, Integration, Performance) and keep integration tests out of unit-only runs.
- Keep tests offline and deterministic; prefer TestServer/TestHost with fixed inputs.
## Quality and safety
- ASCII-only strings and comments unless explicitly justified.
- Clean up temp files/directories created during tests.

View File

@@ -0,0 +1,35 @@
using StellaOps.Provcache;
namespace StellaOps.Provcache.Valkey.Tests;
internal static class ProvcacheEntryFactory
{
public static ProvcacheEntry Create(string veriKey, DateTimeOffset createdAt, DateTimeOffset expiresAt)
{
var digest = new DecisionDigest
{
DigestVersion = "v1",
VeriKey = veriKey,
VerdictHash = "sha256:verdict",
ProofRoot = "sha256:proof",
ReplaySeed = new ReplaySeed { FeedIds = ["feed-1"], RuleIds = ["rule-1"] },
CreatedAt = createdAt,
ExpiresAt = expiresAt,
TrustScore = 72,
TrustScoreBreakdown = TrustScoreBreakdown.CreateDefault(70, 80, 60, 90, 65)
};
return new ProvcacheEntry
{
VeriKey = veriKey,
Decision = digest,
PolicyHash = "sha256:policy",
SignerSetHash = "sha256:signer",
FeedEpoch = "2026-01-01",
CreatedAt = createdAt,
ExpiresAt = expiresAt,
HitCount = 0,
LastAccessedAt = null
};
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.Provcache.Valkey/StellaOps.Provcache.Valkey.csproj" />
<ProjectReference Include="../../StellaOps.Provcache/StellaOps.Provcache.csproj" />
<ProjectReference Include="../../StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
# Provcache Valkey Tests Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| REMED-08 | DONE | Added unit tests for Valkey cache store operations; `dotnet test src/__Libraries/__Tests/StellaOps.Provcache.Valkey.Tests/StellaOps.Provcache.Valkey.Tests.csproj` passed (11 tests) 2026-02-03. |

View File

@@ -0,0 +1,49 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using System.Text.Json;
using Xunit;
namespace StellaOps.Provcache.Valkey.Tests;
public sealed class ValkeyProvcacheStoreGetManyTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetManyAsyncReturnsEmptyWhenNoKeys()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
var result = await harness.Store.GetManyAsync(Array.Empty<string>());
Assert.Empty(result.Hits);
Assert.Empty(result.Misses);
Assert.Equal(0, result.ElapsedMs);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetManyAsyncReturnsHitsAndMisses()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
var entry = ProvcacheEntryFactory.Create("veri-1", now, now.AddMinutes(5));
var payload = JsonSerializer.Serialize(entry, ValkeyProvcacheStoreHarness.JsonOptions);
RedisKey[] capturedKeys = Array.Empty<RedisKey>();
harness.Database
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey[]>(), It.IsAny<CommandFlags>()))
.Callback<RedisKey[], CommandFlags>((keys, _) => capturedKeys = keys)
.ReturnsAsync(new RedisValue[] { RedisValue.Null, payload });
var result = await harness.Store.GetManyAsync(new[] { "missing", "veri-1" });
Assert.Single(result.Hits);
Assert.Single(result.Misses);
Assert.True(result.Hits.ContainsKey("veri-1"));
Assert.Equal("missing", result.Misses[0]);
Assert.Equal($"{harness.Settings.ValkeyKeyPrefix}missing", capturedKeys[0].ToString());
Assert.Equal($"{harness.Settings.ValkeyKeyPrefix}veri-1", capturedKeys[1].ToString());
}
}

View File

@@ -0,0 +1,69 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using System.Text.Json;
using Xunit;
namespace StellaOps.Provcache.Valkey.Tests;
public sealed class ValkeyProvcacheStoreGetOrSetTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetOrSetAsyncUsesFactoryOnMiss()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
var entry = ProvcacheEntryFactory.Create("veri-1", now, now.AddMinutes(5));
var factoryCalls = 0;
harness.Database
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.ReturnsAsync(RedisValue.Null);
harness.Database
.Setup(db => db.StringSetAsync(
It.IsAny<RedisKey>(),
It.IsAny<RedisValue>(),
It.IsAny<TimeSpan?>(),
It.IsAny<When>(),
It.IsAny<CommandFlags>()))
.ReturnsAsync(true);
var result = await harness.Store.GetOrSetAsync(
"veri-1",
_ =>
{
factoryCalls++;
return ValueTask.FromResult(entry);
});
Assert.Equal(1, factoryCalls);
Assert.Equal(entry.VeriKey, result.VeriKey);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetOrSetAsyncSkipsFactoryOnHit()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
var entry = ProvcacheEntryFactory.Create("veri-2", now, now.AddMinutes(5));
var payload = JsonSerializer.Serialize(entry, ValkeyProvcacheStoreHarness.JsonOptions);
var factoryCalls = 0;
harness.Database
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.ReturnsAsync(payload);
var result = await harness.Store.GetOrSetAsync(
"veri-2",
_ =>
{
factoryCalls++;
return ValueTask.FromResult(entry);
});
Assert.Equal(0, factoryCalls);
Assert.Equal(entry.VeriKey, result.VeriKey);
}
}

View File

@@ -0,0 +1,56 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using System.Text.Json;
using Xunit;
namespace StellaOps.Provcache.Valkey.Tests;
public sealed class ValkeyProvcacheStoreGetTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetAsyncReturnsMissWhenValueMissing()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
RedisKey capturedKey = default;
var captured = false;
harness.Database
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.Callback<RedisKey, CommandFlags>((key, _) =>
{
capturedKey = key;
captured = true;
})
.ReturnsAsync(RedisValue.Null);
var result = await harness.Store.GetAsync("veri-1");
Assert.False(result.IsHit);
Assert.Null(result.Entry);
Assert.True(captured);
Assert.Equal($"{harness.Settings.ValkeyKeyPrefix}veri-1", capturedKey.ToString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetAsyncReturnsHitWhenValueExists()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
var entry = ProvcacheEntryFactory.Create("veri-2", now, now.AddMinutes(5));
var payload = JsonSerializer.Serialize(entry, ValkeyProvcacheStoreHarness.JsonOptions);
harness.Database
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.ReturnsAsync(payload);
var result = await harness.Store.GetAsync("veri-2");
Assert.True(result.IsHit);
Assert.NotNull(result.Entry);
Assert.Equal(entry.VeriKey, result.Entry!.VeriKey);
}
}

View File

@@ -0,0 +1,62 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using System.Net;
using Xunit;
namespace StellaOps.Provcache.Valkey.Tests;
public sealed class ValkeyProvcacheStoreInvalidateTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task InvalidateAsyncReturnsDeleteResult()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
harness.Database
.Setup(db => db.KeyDeleteAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.ReturnsAsync(true);
var result = await harness.Store.InvalidateAsync("veri-1");
Assert.True(result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task InvalidateByPatternAsyncUsesPrefix()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
var endpoint = new DnsEndPoint("localhost", 6379);
var server = new Mock<IServer>();
var keys = new[] { (RedisKey)"stellaops:prov:match:1", (RedisKey)"stellaops:prov:match:2" };
var capturedPattern = string.Empty;
server.SetupGet(s => s.IsConnected).Returns(true);
server
.Setup(s => s.Keys(
It.IsAny<int>(),
It.IsAny<RedisValue>(),
It.IsAny<int>(),
It.IsAny<long>(),
It.IsAny<int>(),
It.IsAny<CommandFlags>()))
.Callback<int, RedisValue, int, long, int, CommandFlags>((_, pattern, _, _, _, _) =>
capturedPattern = pattern.ToString())
.Returns(keys);
harness.Multiplexer.Setup(m => m.GetEndPoints(It.IsAny<bool>())).Returns(new EndPoint[] { endpoint });
harness.Multiplexer.Setup(m => m.GetServer(endpoint, It.IsAny<object?>())).Returns(server.Object);
harness.Database
.Setup(db => db.KeyDeleteAsync(It.IsAny<RedisKey[]>(), It.IsAny<CommandFlags>()))
.ReturnsAsync((RedisKey[] batch, CommandFlags _) => batch.LongLength);
var deleted = await harness.Store.InvalidateByPatternAsync("match:*");
Assert.Equal(2, deleted);
Assert.Equal($"{harness.Settings.ValkeyKeyPrefix}match:*", capturedPattern);
}
}

View File

@@ -0,0 +1,48 @@
using Moq;
using StackExchange.Redis;
using StellaOps.Provcache;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Provcache.Valkey.Tests;
public sealed class ValkeyProvcacheStoreSetManyTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task SetManyAsyncBatchesAndCapsTtl()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var options = new ProvcacheOptions
{
ValkeyKeyPrefix = "stellaops:prov:",
MaxTtl = TimeSpan.FromMinutes(5)
};
var harness = new ValkeyProvcacheStoreHarness(now, options);
var batch = new Mock<IBatch>();
batch
.Setup(b => b.StringSetAsync(
It.IsAny<RedisKey>(),
It.IsAny<RedisValue>(),
It.IsAny<TimeSpan?>(),
It.IsAny<When>(),
It.IsAny<CommandFlags>()))
.ReturnsAsync(true);
harness.Database.Setup(db => db.CreateBatch(It.IsAny<object?>())).Returns(batch.Object);
var entries = new[]
{
ProvcacheEntryFactory.Create("veri-1", now, now.AddHours(1)),
ProvcacheEntryFactory.Create("veri-expired", now.AddHours(-2), now.AddMinutes(-1))
};
await harness.Store.SetManyAsync(entries);
batch.Verify(b => b.Execute(), Times.Once);
var invocation = Assert.Single(batch.Invocations, i => i.Method.Name == "StringSetAsync");
Assert.NotNull(invocation.Arguments[2]);
var ttl = (Expiration)invocation.Arguments[2]!;
Assert.Equal((Expiration)options.MaxTtl, ttl);
}
}

View File

@@ -0,0 +1,58 @@
using Moq;
using StackExchange.Redis;
using StellaOps.Provcache;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Provcache.Valkey.Tests;
public sealed class ValkeyProvcacheStoreSetTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task SetAsyncCapsTtlAtMax()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var options = new ProvcacheOptions
{
ValkeyKeyPrefix = "stellaops:prov:",
MaxTtl = TimeSpan.FromMinutes(5)
};
var harness = new ValkeyProvcacheStoreHarness(now, options);
harness.Database
.Setup(db => db.StringSetAsync(
It.IsAny<RedisKey>(),
It.IsAny<RedisValue>(),
It.IsAny<TimeSpan?>(),
It.IsAny<When>(),
It.IsAny<CommandFlags>()))
.ReturnsAsync(true);
var entry = ProvcacheEntryFactory.Create("veri-1", now, now.AddHours(1));
await harness.Store.SetAsync(entry);
var invocation = Assert.Single(harness.Database.Invocations, i => i.Method.Name == "StringSetAsync");
Assert.NotNull(invocation.Arguments[2]);
var ttl = (Expiration)invocation.Arguments[2]!;
Assert.Equal((Expiration)options.MaxTtl, ttl);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task SetAsyncSkipsExpiredEntries()
{
var now = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero);
var harness = new ValkeyProvcacheStoreHarness(now);
var entry = ProvcacheEntryFactory.Create("veri-2", now.AddHours(-2), now.AddMinutes(-1));
await harness.Store.SetAsync(entry);
harness.Database.Verify(db => db.StringSetAsync(
It.IsAny<RedisKey>(),
It.IsAny<RedisValue>(),
It.IsAny<TimeSpan?>(),
It.IsAny<When>(),
It.IsAny<CommandFlags>()), Times.Never);
}
}

View File

@@ -0,0 +1,52 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using StackExchange.Redis;
using StellaOps.Provcache;
using StellaOps.Provcache.Valkey;
using System.Text.Json;
namespace StellaOps.Provcache.Valkey.Tests;
internal sealed class FixedTimeProvider : TimeProvider
{
public FixedTimeProvider(DateTimeOffset utcNow) => UtcNow = utcNow;
public DateTimeOffset UtcNow { get; set; }
public override DateTimeOffset GetUtcNow() => UtcNow;
}
internal sealed class ValkeyProvcacheStoreHarness
{
public static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
public Mock<IConnectionMultiplexer> Multiplexer { get; } = new();
public Mock<IDatabase> Database { get; } = new();
public Mock<ILogger<ValkeyProvcacheStore>> Logger { get; } = new();
public FixedTimeProvider TimeProvider { get; }
public ProvcacheOptions Settings { get; }
public ValkeyProvcacheStore Store { get; }
public ValkeyProvcacheStoreHarness(DateTimeOffset utcNow, ProvcacheOptions? options = null)
{
Settings = options ?? new ProvcacheOptions
{
ValkeyKeyPrefix = "stellaops:prov:",
MaxTtl = TimeSpan.FromMinutes(10)
};
TimeProvider = new FixedTimeProvider(utcNow);
Multiplexer.Setup(m => m.GetDatabase(It.IsAny<int>(), It.IsAny<object?>()))
.Returns(Database.Object);
Store = new ValkeyProvcacheStore(
Multiplexer.Object,
Options.Create(Settings),
Logger.Object,
TimeProvider);
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"parallelizeTestCollections": false,
"maxParallelThreads": 1
}