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,31 @@
# StellaOps.ReachGraph.Cache.Tests - Local Agent Charter
## Roles
- Backend developer
- QA automation engineer
## Working directory
- src/__Libraries/__Tests/StellaOps.ReachGraph.Cache.Tests
## Allowed dependencies
- src/__Libraries/StellaOps.ReachGraph.Cache
- src/__Libraries/StellaOps.ReachGraph
- src/__Libraries/StellaOps.TestKit
## Required reading
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/platform/architecture-overview.md
- docs/modules/reach-graph/README.md
- docs/modules/reach-graph/architecture.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,26 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using System.Threading;
using Xunit;
namespace StellaOps.ReachGraph.Cache.Tests;
public sealed class ReachGraphValkeyCacheCancellationTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetAsyncHonorsCancellation()
{
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(
() => harness.Cache.GetAsync("sha256:graph", cts.Token));
harness.Database.Verify(
db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()),
Times.Never);
}
}

View File

@@ -0,0 +1,24 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.ReachGraph.Cache.Tests;
public sealed class ReachGraphValkeyCacheExistsTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task ExistsAsyncReturnsTrueWhenKeyExists()
{
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
harness.Database
.Setup(db => db.KeyExistsAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.ReturnsAsync(true);
var result = await harness.Cache.ExistsAsync("sha256:graph");
Assert.True(result);
}
}

View File

@@ -0,0 +1,47 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.ReachGraph.Cache.Tests;
public sealed class ReachGraphValkeyCacheGetTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetAsyncReturnsNullWhenMissing()
{
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
RedisKey capturedKey = default;
harness.Database
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.Callback<RedisKey, CommandFlags>((key, _) => capturedKey = key)
.ReturnsAsync(RedisValue.Null);
var result = await harness.Cache.GetAsync("sha256:graph");
Assert.Null(result);
Assert.Equal("reachgraph:tenant-1:sha256:graph", capturedKey.ToString());
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetAsyncReturnsGraphWhenHit()
{
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
var graph = ReachGraphValkeyCacheTestData.CreateGraph("sha256:graph");
var json = harness.Serializer.SerializeMinimal(graph);
var stored = ReachGraphValkeyCacheTestData.Compress(json);
harness.Database
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.ReturnsAsync(stored);
var result = await harness.Cache.GetAsync("sha256:graph");
Assert.NotNull(result);
Assert.Equal("sha256:graph", result!.Artifact.Digest);
Assert.Equal("reachgraph.min@v1", result.SchemaVersion);
}
}

View File

@@ -0,0 +1,41 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using StackExchange.Redis;
using StellaOps.ReachGraph.Serialization;
namespace StellaOps.ReachGraph.Cache.Tests;
internal sealed class ReachGraphValkeyCacheHarness
{
public Mock<IConnectionMultiplexer> Multiplexer { get; } = new();
public Mock<IDatabase> Database { get; } = new();
public Mock<ILogger<ReachGraphValkeyCache>> Logger { get; } = new();
public CanonicalReachGraphSerializer Serializer { get; } = new();
public ReachGraphCacheOptions Settings { get; }
public ReachGraphValkeyCache Cache { get; }
public ReachGraphValkeyCacheHarness(string tenantId, ReachGraphCacheOptions? options = null)
{
Settings = options ?? new ReachGraphCacheOptions
{
KeyPrefix = "reachgraph",
DefaultTtl = TimeSpan.FromMinutes(30),
SliceTtl = TimeSpan.FromMinutes(10),
MaxGraphSizeBytes = 1024 * 1024,
CompressInCache = true,
Database = 0
};
Multiplexer
.Setup(m => m.GetDatabase(It.IsAny<int>(), It.IsAny<object?>()))
.Returns(Database.Object);
Cache = new ReachGraphValkeyCache(
Multiplexer.Object,
Serializer,
Options.Create(Settings),
Logger.Object,
tenantId);
}
}

View File

@@ -0,0 +1,71 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using System.Net;
using Xunit;
namespace StellaOps.ReachGraph.Cache.Tests;
public sealed class ReachGraphValkeyCacheInvalidateTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task InvalidateAsyncDeletesGraphAndSlices()
{
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
var endpoint1 = new DnsEndPoint("localhost", 6379);
var endpoint2 = new DnsEndPoint("localhost", 6380);
var server1 = new Mock<IServer>();
var server2 = new Mock<IServer>();
var keys1 = new[] { (RedisKey)"reachgraph:tenant-1:sha256:graph:slice:a" };
var keys2 = new[] { (RedisKey)"reachgraph:tenant-1:sha256:graph:slice:b" };
var capturedPattern = string.Empty;
RedisKey capturedGraphKey = default;
RedisKey[]? capturedSliceKeys = null;
server1.SetupGet(s => s.IsConnected).Returns(true);
server1
.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(keys1);
server2.SetupGet(s => s.IsConnected).Returns(true);
server2
.Setup(s => s.Keys(
It.IsAny<int>(),
It.IsAny<RedisValue>(),
It.IsAny<int>(),
It.IsAny<long>(),
It.IsAny<int>(),
It.IsAny<CommandFlags>()))
.Returns(keys2);
harness.Multiplexer.Setup(m => m.GetEndPoints(It.IsAny<bool>())).Returns(
new EndPoint[] { endpoint1, endpoint2 });
harness.Multiplexer.Setup(m => m.GetServer(endpoint1, It.IsAny<object?>())).Returns(server1.Object);
harness.Multiplexer.Setup(m => m.GetServer(endpoint2, It.IsAny<object?>())).Returns(server2.Object);
harness.Database
.Setup(db => db.KeyDeleteAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.Callback<RedisKey, CommandFlags>((key, _) => capturedGraphKey = key)
.ReturnsAsync(true);
harness.Database
.Setup(db => db.KeyDeleteAsync(It.IsAny<RedisKey[]>(), It.IsAny<CommandFlags>()))
.Callback<RedisKey[], CommandFlags>((keys, _) => capturedSliceKeys = keys)
.ReturnsAsync((RedisKey[] keys, CommandFlags _) => keys.LongLength);
await harness.Cache.InvalidateAsync("sha256:graph");
Assert.Equal("reachgraph:tenant-1:sha256:graph", capturedGraphKey.ToString());
Assert.Equal("reachgraph:tenant-1:sha256:graph:slice:*", capturedPattern);
Assert.NotNull(capturedSliceKeys);
Assert.Contains(keys1[0], capturedSliceKeys!);
Assert.Contains(keys2[0], capturedSliceKeys!);
}
}

View File

@@ -0,0 +1,62 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.ReachGraph.Cache.Tests;
public sealed class ReachGraphValkeyCacheSetTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task SetAsyncStoresCompressedGraph()
{
var options = new ReachGraphCacheOptions { DefaultTtl = TimeSpan.FromMinutes(5) };
var harness = new ReachGraphValkeyCacheHarness("tenant-1", options);
var graph = ReachGraphValkeyCacheTestData.CreateGraph("sha256:graph");
harness.Database
.Setup(db => db.StringSetAsync(
It.IsAny<RedisKey>(),
It.IsAny<RedisValue>(),
It.IsAny<TimeSpan?>(),
It.IsAny<When>(),
It.IsAny<CommandFlags>()))
.ReturnsAsync(true);
await harness.Cache.SetAsync("sha256:graph", graph, ttl: TimeSpan.FromMinutes(2));
var invocation = Assert.Single(harness.Database.Invocations, i => i.Method.Name == "StringSetAsync");
var key = (RedisKey)invocation.Arguments[0]!;
var storedValue = (RedisValue)invocation.Arguments[1]!;
var stored = (byte[]?)storedValue;
var ttl = (Expiration)invocation.Arguments[2]!;
var json = harness.Serializer.SerializeMinimal(graph);
Assert.NotNull(stored);
var decompressed = ReachGraphValkeyCacheTestData.Decompress(stored);
Assert.Equal("reachgraph:tenant-1:sha256:graph", key.ToString());
Assert.Equal((Expiration)TimeSpan.FromMinutes(2), ttl);
Assert.Equal(json, decompressed);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task SetAsyncSkipsWhenGraphTooLarge()
{
var options = new ReachGraphCacheOptions { MaxGraphSizeBytes = 1 };
var harness = new ReachGraphValkeyCacheHarness("tenant-1", options);
var graph = ReachGraphValkeyCacheTestData.CreateGraph("sha256:graph");
await harness.Cache.SetAsync("sha256:graph", graph);
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,57 @@
using Moq;
using StackExchange.Redis;
using StellaOps.TestKit;
using System.Text;
using Xunit;
namespace StellaOps.ReachGraph.Cache.Tests;
public sealed class ReachGraphValkeyCacheSliceTests
{
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task GetSliceAsyncReturnsBytesWhenHit()
{
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
var slice = Encoding.UTF8.GetBytes("slice-data");
var stored = ReachGraphValkeyCacheTestData.Compress(slice);
harness.Database
.Setup(db => db.StringGetAsync(It.IsAny<RedisKey>(), It.IsAny<CommandFlags>()))
.ReturnsAsync(stored);
var result = await harness.Cache.GetSliceAsync("sha256:graph", "slice-1");
Assert.NotNull(result);
Assert.Equal(slice, result);
}
[Fact]
[Trait("Category", TestCategories.Unit)]
public async Task SetSliceAsyncStoresCompressedSlice()
{
var harness = new ReachGraphValkeyCacheHarness("tenant-1");
var slice = Encoding.UTF8.GetBytes("slice-data");
harness.Database
.Setup(db => db.StringSetAsync(
It.IsAny<RedisKey>(),
It.IsAny<RedisValue>(),
It.IsAny<TimeSpan?>(),
It.IsAny<When>(),
It.IsAny<CommandFlags>()))
.ReturnsAsync(true);
await harness.Cache.SetSliceAsync("sha256:graph", "slice-1", slice, ttl: TimeSpan.FromMinutes(1));
var invocation = Assert.Single(harness.Database.Invocations, i => i.Method.Name == "StringSetAsync");
var storedValue = (RedisValue)invocation.Arguments[1]!;
var stored = (byte[]?)storedValue;
var ttl = (Expiration)invocation.Arguments[2]!;
Assert.NotNull(stored);
var decompressed = ReachGraphValkeyCacheTestData.Decompress(stored);
Assert.Equal(slice, decompressed);
Assert.Equal((Expiration)TimeSpan.FromMinutes(1), ttl);
}
}

View File

@@ -0,0 +1,68 @@
using System.Collections.Immutable;
using System.IO;
using System.IO.Compression;
using StellaOps.ReachGraph.Schema;
namespace StellaOps.ReachGraph.Cache.Tests;
internal static class ReachGraphValkeyCacheTestData
{
public static ReachGraphMinimal CreateGraph(string digest)
{
return new ReachGraphMinimal
{
Artifact = new ReachGraphArtifact(
"svc.payments",
digest,
ImmutableArray.Create("linux/amd64")),
Scope = new ReachGraphScope(
ImmutableArray.Create("/app/bin/svc"),
ImmutableArray.Create("prod")),
Nodes = ImmutableArray.Create(new ReachGraphNode
{
Id = "node-1",
Kind = ReachGraphNodeKind.Package,
Ref = "pkg:npm/lodash@4.17.21",
IsEntrypoint = true
}),
Edges = ImmutableArray.Create(new ReachGraphEdge
{
From = "node-1",
To = "node-1",
Why = new EdgeExplanation
{
Type = EdgeExplanationType.Import,
Confidence = 1.0
}
}),
Provenance = new ReachGraphProvenance
{
Inputs = new ReachGraphInputs
{
Sbom = "sha256:sbom"
},
ComputedAt = new DateTimeOffset(2026, 1, 2, 3, 4, 5, TimeSpan.Zero),
Analyzer = new ReachGraphAnalyzer("reach-graph", "1.0.0", "sha256:tool")
}
};
}
public static byte[] Compress(byte[] data)
{
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionLevel.Fastest, leaveOpen: true))
{
gzip.Write(data);
}
return output.ToArray();
}
public static byte[] Decompress(byte[] compressed)
{
using var input = new MemoryStream(compressed);
using var gzip = new GZipStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
gzip.CopyTo(output);
return output.ToArray();
}
}

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.ReachGraph.Cache/StellaOps.ReachGraph.Cache.csproj" />
<ProjectReference Include="../../StellaOps.ReachGraph/StellaOps.ReachGraph.csproj" />
<ProjectReference Include="../../StellaOps.TestKit/StellaOps.TestKit.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

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