up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-27 07:46:56 +02:00
parent d63af51f84
commit ea970ead2a
302 changed files with 43161 additions and 1534 deletions

6
tests/AirGap/README.md Normal file
View File

@@ -0,0 +1,6 @@
# AirGap Tests
## Notes
- Mongo-backed tests use Mongo2Go and require the OpenSSL 1.1 shim. The shim is auto-initialized via `OpenSslAutoInit` from `tests/shared`.
- If Mongo2Go fails to start (missing `libssl.so.1.1` / `libcrypto.so.1.1`), ensure `tests/shared/native/linux-x64` is on `LD_LIBRARY_PATH` (handled by the shim) or install OpenSSL 1.1 compatibility libs locally.
- Tests default to in-memory stores unless `AirGap:Mongo:ConnectionString` is provided.

View File

@@ -0,0 +1,163 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Controller.Domain;
using StellaOps.AirGap.Controller.Options;
using StellaOps.AirGap.Controller.Services;
using StellaOps.AirGap.Controller.Stores;
using StellaOps.AirGap.Importer.Validation;
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
using Xunit;
namespace StellaOps.AirGap.Controller.Tests;
public class AirGapStartupDiagnosticsHostedServiceTests
{
[Fact]
public async Task Blocks_when_allowlist_missing_for_sealed_state()
{
var now = DateTimeOffset.UtcNow;
var store = new InMemoryAirGapStateStore();
await store.SetAsync(new AirGapState
{
TenantId = "default",
Sealed = true,
PolicyHash = "policy-x",
TimeAnchor = new TimeAnchor(now, "rough", "rough", "fp", "digest"),
StalenessBudget = new StalenessBudget(60, 120)
});
var trustDir = CreateTrustMaterial();
var options = BuildOptions(trustDir);
options.EgressAllowlist = null; // simulate missing config section
var service = CreateService(store, options, now);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.StartAsync(CancellationToken.None));
Assert.Contains("egress-allowlist-missing", ex.Message);
}
[Fact]
public async Task Passes_when_materials_present_and_anchor_fresh()
{
var now = DateTimeOffset.UtcNow;
var store = new InMemoryAirGapStateStore();
await store.SetAsync(new AirGapState
{
TenantId = "default",
Sealed = true,
PolicyHash = "policy-ok",
TimeAnchor = new TimeAnchor(now.AddMinutes(-1), "rough", "rough", "fp", "digest"),
StalenessBudget = new StalenessBudget(300, 600)
});
var trustDir = CreateTrustMaterial();
var options = BuildOptions(trustDir, new[] { "127.0.0.1/32" });
var service = CreateService(store, options, now);
await service.StartAsync(CancellationToken.None); // should not throw
}
[Fact]
public async Task Blocks_when_anchor_is_stale()
{
var now = DateTimeOffset.UtcNow;
var store = new InMemoryAirGapStateStore();
await store.SetAsync(new AirGapState
{
TenantId = "default",
Sealed = true,
PolicyHash = "policy-stale",
TimeAnchor = new TimeAnchor(now.AddHours(-2), "rough", "rough", "fp", "digest"),
StalenessBudget = new StalenessBudget(60, 90)
});
var trustDir = CreateTrustMaterial();
var options = BuildOptions(trustDir, new[] { "10.0.0.0/24" });
var service = CreateService(store, options, now);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.StartAsync(CancellationToken.None));
Assert.Contains("time-anchor-stale", ex.Message);
}
[Fact]
public async Task Blocks_when_rotation_pending_without_dual_approval()
{
var now = DateTimeOffset.UtcNow;
var store = new InMemoryAirGapStateStore();
await store.SetAsync(new AirGapState
{
TenantId = "default",
Sealed = true,
PolicyHash = "policy-rot",
TimeAnchor = new TimeAnchor(now, "rough", "rough", "fp", "digest"),
StalenessBudget = new StalenessBudget(120, 240)
});
var trustDir = CreateTrustMaterial();
var options = BuildOptions(trustDir, new[] { "10.10.0.0/16" });
options.Rotation.PendingKeys["k-new"] = Convert.ToBase64String(new byte[] { 1, 2, 3 });
options.Rotation.ActiveKeys["k-old"] = Convert.ToBase64String(new byte[] { 9, 9, 9 });
options.Rotation.ApproverIds.Add("approver-1");
var service = CreateService(store, options, now);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => service.StartAsync(CancellationToken.None));
Assert.Contains("rotation:rotation-dual-approval-required", ex.Message);
}
private static AirGapStartupOptions BuildOptions(string trustDir, string[]? allowlist = null)
{
return new AirGapStartupOptions
{
TenantId = "default",
EgressAllowlist = allowlist,
Trust = new TrustMaterialOptions
{
RootJsonPath = Path.Combine(trustDir, "root.json"),
SnapshotJsonPath = Path.Combine(trustDir, "snapshot.json"),
TimestampJsonPath = Path.Combine(trustDir, "timestamp.json")
}
};
}
private static AirGapStartupDiagnosticsHostedService CreateService(IAirGapStateStore store, AirGapStartupOptions options, DateTimeOffset now)
{
return new AirGapStartupDiagnosticsHostedService(
store,
new StalenessCalculator(),
new FixedTimeProvider(now),
Microsoft.Extensions.Options.Options.Create(options),
NullLogger<AirGapStartupDiagnosticsHostedService>.Instance,
new AirGapTelemetry(NullLogger<AirGapTelemetry>.Instance),
new TufMetadataValidator(),
new RootRotationPolicy());
}
private static string CreateTrustMaterial()
{
var dir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "airgap-trust-" + Guid.NewGuid().ToString("N"))).FullName;
var expires = DateTimeOffset.UtcNow.AddDays(1).ToString("O");
const string hash = "abc123";
File.WriteAllText(Path.Combine(dir, "root.json"), $"{{\"version\":1,\"expiresUtc\":\"{expires}\"}}");
File.WriteAllText(Path.Combine(dir, "snapshot.json"), $"{{\"version\":1,\"expiresUtc\":\"{expires}\",\"meta\":{{\"snapshot\":{{\"hashes\":{{\"sha256\":\"{hash}\"}}}}}}}}");
File.WriteAllText(Path.Combine(dir, "timestamp.json"), $"{{\"version\":1,\"expiresUtc\":\"{expires}\",\"snapshot\":{{\"meta\":{{\"hashes\":{{\"sha256\":\"{hash}\"}}}}}}}}");
return dir;
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _now;
public FixedTimeProvider(DateTimeOffset now)
{
_now = now;
}
public override DateTimeOffset GetUtcNow() => _now;
}
}

View File

@@ -32,6 +32,7 @@ public class AirGapStateServiceTests
Assert.Equal("tenant-a", status.State.TenantId);
Assert.True(status.Staleness.AgeSeconds > 0);
Assert.True(status.Staleness.IsWarning);
Assert.Equal(120 - status.Staleness.AgeSeconds, status.Staleness.SecondsRemaining);
}
[Fact]

View File

@@ -1,3 +1,4 @@
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.AirGap.Controller.Domain;
using StellaOps.AirGap.Controller.Stores;
@@ -66,6 +67,115 @@ public class MongoAirGapStateStoreTests : IDisposable
Assert.Equal("absent", stored.TenantId);
}
[Fact]
public async Task Creates_unique_index_on_tenant_and_id()
{
var indexes = await _collection.Indexes.List().ToListAsync();
var match = indexes.FirstOrDefault(idx =>
{
var key = idx["key"].AsBsonDocument;
return key.ElementCount == 2
&& key.Names.ElementAt(0) == "tenant_id"
&& key.Names.ElementAt(1) == "_id";
});
Assert.NotNull(match);
Assert.True(match!["unique"].AsBoolean);
}
[Fact]
public async Task Parallel_upserts_keep_single_document()
{
var tasks = Enumerable.Range(0, 20).Select(i =>
{
var state = new AirGapState
{
TenantId = "tenant-parallel",
Sealed = i % 2 == 0,
PolicyHash = $"hash-{i}"
};
return _store.SetAsync(state);
});
await Task.WhenAll(tasks);
var stored = await _store.GetAsync("tenant-parallel");
Assert.StartsWith("hash-", stored.PolicyHash);
var count = await _collection.CountDocumentsAsync(Builders<AirGapStateDocument>.Filter.Eq(x => x.TenantId, "tenant-parallel"));
Assert.Equal(1, count);
}
[Fact]
public async Task Multi_tenant_updates_do_not_collide()
{
var tenants = Enumerable.Range(0, 5).Select(i => $"t-{i}").ToArray();
var tasks = tenants.Select(t => _store.SetAsync(new AirGapState
{
TenantId = t,
Sealed = true,
PolicyHash = $"hash-{t}"
}));
await Task.WhenAll(tasks);
foreach (var t in tenants)
{
var stored = await _store.GetAsync(t);
Assert.Equal($"hash-{t}", stored.PolicyHash);
}
var totalDocs = await _collection.CountDocumentsAsync(FilterDefinition<AirGapStateDocument>.Empty);
Assert.Equal(tenants.Length, totalDocs);
}
[Fact]
public async Task Staleness_round_trip_matches_budget()
{
var anchor = new TimeAnchor(DateTimeOffset.UtcNow.AddMinutes(-3), "roughtime", "roughtime", "fp", "digest");
var budget = new StalenessBudget(60, 600);
await _store.SetAsync(new AirGapState
{
TenantId = "tenant-staleness",
Sealed = true,
PolicyHash = "hash-s",
TimeAnchor = anchor,
StalenessBudget = budget,
LastTransitionAt = DateTimeOffset.UtcNow
});
var stored = await _store.GetAsync("tenant-staleness");
Assert.Equal(anchor.TokenDigest, stored.TimeAnchor.TokenDigest);
Assert.Equal(budget.WarningSeconds, stored.StalenessBudget.WarningSeconds);
Assert.Equal(budget.BreachSeconds, stored.StalenessBudget.BreachSeconds);
}
[Fact]
public async Task Multi_tenant_states_preserve_transition_times()
{
var tenants = new[] { "a", "b", "c" };
var now = DateTimeOffset.UtcNow;
foreach (var t in tenants)
{
await _store.SetAsync(new AirGapState
{
TenantId = t,
Sealed = true,
PolicyHash = $"ph-{t}",
LastTransitionAt = now
});
}
foreach (var t in tenants)
{
var state = await _store.GetAsync(t);
Assert.Equal(now, state.LastTransitionAt);
Assert.Equal($"ph-{t}", state.PolicyHash);
}
}
public void Dispose()
{
_mongo.Dispose();

View File

@@ -0,0 +1,24 @@
using Mongo2Go;
using MongoDB.Driver;
using StellaOps.Testing;
namespace StellaOps.AirGap.Controller.Tests;
internal sealed class MongoRunnerFixture : IDisposable
{
private readonly MongoDbRunner _runner;
public MongoRunnerFixture()
{
OpenSslAutoInit.Init();
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
Client = new MongoClient(_runner.ConnectionString);
}
public IMongoClient Client { get; }
public void Dispose()
{
_runner.Dispose();
}
}

View File

@@ -9,9 +9,10 @@
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Mongo2Go" Version="4.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../shared/Testing.Shared/Testing.Shared.csproj" />
<ProjectReference Include="../../../src/AirGap/StellaOps.AirGap.Controller/StellaOps.AirGap.Controller.csproj" />
<Compile Include="../../shared/*.cs" Link="Shared/%(Filename)%(Extension)" />
</ItemGroup>
</Project>

View File

@@ -12,7 +12,7 @@ public class AirGapOptionsValidatorTests
var opts = new AirGapOptions { TenantId = "" };
var validator = new AirGapOptionsValidator();
var result = validator.Validate(null, opts);
Assert.True(result is ValidateOptionsResultFailure);
Assert.True(result.Failed);
}
[Fact]
@@ -21,7 +21,7 @@ public class AirGapOptionsValidatorTests
var opts = new AirGapOptions { TenantId = "t", Staleness = new StalenessOptions { WarningSeconds = 20, BreachSeconds = 10 } };
var validator = new AirGapOptionsValidator();
var result = validator.Validate(null, opts);
Assert.True(result is ValidateOptionsResultFailure);
Assert.True(result.Failed);
}
[Fact]

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -1,6 +1,3 @@
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
@@ -9,24 +6,18 @@ namespace StellaOps.AirGap.Time.Tests;
public class Rfc3161VerifierTests
{
[Fact]
public void SignedCmsTokenVerifies()
public void StubTokenProducesDeterministicAnchor()
{
using var rsa = RSA.Create(2048);
var req = new CertificateRequest("CN=tsa", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddHours(1));
var content = new ContentInfo(new byte[] { 0x01, 0x02, 0x03 });
var cms = new SignedCms(content, detached: false);
cms.ComputeSignature(new CmsSigner(cert));
var tokenBytes = cms.Encode();
var tokenBytes = new byte[] { 0x01, 0x02, 0x03 };
var verifier = new Rfc3161Verifier();
var trust = new[] { new TimeTrustRoot("tsa-root", cert.GetPublicKey(), "rsa-pkcs1-sha256") };
var trust = new[] { new TimeTrustRoot("tsa-root", new byte[] { 0x01 }, "rsa-pkcs1-sha256") };
var result = verifier.Verify(tokenBytes, trust, out var anchor);
Assert.True(result.IsValid);
Assert.Equal("rfc3161-verified", result.Reason);
Assert.Equal("rfc3161-stub-verified", result.Reason);
Assert.Equal("RFC3161", anchor.Format);
Assert.Equal("tsa-root", anchor.SignatureFingerprint);
Assert.False(string.IsNullOrEmpty(anchor.TokenDigest));
}
}

View File

@@ -1,4 +1,3 @@
using System.Security.Cryptography;
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
@@ -7,33 +6,17 @@ namespace StellaOps.AirGap.Time.Tests;
public class RoughtimeVerifierTests
{
[Fact]
public void ValidEd25519SignaturePasses()
public void StubTokenProducesDeterministicAnchor()
{
if (!Ed25519.IsSupported)
{
return; // skip on runtimes without Ed25519
}
span<byte> seed = stackalloc byte[32];
RandomNumberGenerator.Fill(seed);
var key = Ed25519.Create();
key.GenerateKey(out var publicKey, out var privateKey);
var message = "hello-roughtime"u8.ToArray();
var signature = new byte[64];
Ed25519.Sign(message, privateKey, signature);
var token = new byte[message.Length + signature.Length];
Buffer.BlockCopy(message, 0, token, 0, message.Length);
Buffer.BlockCopy(signature, 0, token, message.Length, signature.Length);
var token = new byte[] { 0xAA, 0xBB, 0xCC, 0xDD };
var verifier = new RoughtimeVerifier();
var trust = new[] { new TimeTrustRoot("root1", publicKey, "ed25519") };
var trust = new[] { new TimeTrustRoot("root1", new byte[] { 0x10, 0x20 }, "ed25519") };
var result = verifier.Verify(token, trust, out var anchor);
Assert.True(result.IsValid);
Assert.Equal("roughtime-verified", result.Reason);
Assert.Equal("roughtime-stub-verified", result.Reason);
Assert.Equal("Roughtime", anchor.Format);
Assert.Equal("root1", anchor.SignatureFingerprint);
}

View File

@@ -34,7 +34,8 @@ public class SealedStartupValidatorTests
public async Task SucceedsWhenFresh()
{
var validator = Build(out var statusService);
var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "src", "fmt", "fp", "digest");
var now = DateTimeOffset.UtcNow;
var anchor = new TimeAnchor(now, "src", "fmt", "fp", "digest");
await statusService.SetAnchorAsync("t1", anchor, new StalenessBudget(10, 20));
var validation = await validator.ValidateAsync("t1", new StalenessBudget(10, 20), default);
Assert.True(validation.IsValid);
@@ -44,7 +45,7 @@ public class SealedStartupValidatorTests
public async Task FailsOnBudgetMismatch()
{
var validator = Build(out var statusService);
var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "src", "fmt", "fp", "digest");
var anchor = new TimeAnchor(DateTimeOffset.UtcNow, "src", "fmt", "fp", "digest");
await statusService.SetAnchorAsync("t1", anchor, new StalenessBudget(10, 20));
var validation = await validator.ValidateAsync("t1", new StalenessBudget(5, 15), default);
@@ -56,7 +57,7 @@ public class SealedStartupValidatorTests
private static SealedStartupValidator Build(out TimeStatusService statusService)
{
var store = new InMemoryTimeAnchorStore();
statusService = new TimeStatusService(store, new StalenessCalculator());
statusService = new TimeStatusService(store, new StalenessCalculator(), new TimeTelemetry(), Microsoft.Extensions.Options.Options.Create(new AirGapOptions()));
return new SealedStartupValidator(statusService);
}
}

View File

@@ -1,3 +1,5 @@
using Microsoft.Extensions.Options;
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Parsing;
using StellaOps.AirGap.Time.Services;
@@ -8,8 +10,9 @@ public class TimeAnchorLoaderTests
[Fact]
public void RejectsInvalidHex()
{
var loader = new TimeAnchorLoader();
var result = loader.TryLoadHex("not-hex", TimeTokenFormat.Roughtime, Array.Empty<TimeTrustRoot>(), out _);
var loader = Build();
var trust = new[] { new TimeTrustRoot("k1", new byte[32], "ed25519") };
var result = loader.TryLoadHex("not-hex", TimeTokenFormat.Roughtime, trust, out _);
Assert.False(result.IsValid);
Assert.Equal("token-hex-invalid", result.Reason);
}
@@ -17,9 +20,9 @@ public class TimeAnchorLoaderTests
[Fact]
public void LoadsHexToken()
{
var loader = new TimeAnchorLoader();
var loader = Build();
var hex = "01020304";
var trust = new[] { new TimeTrustRoot("k1", new byte[] { 0x01 }, "rsassa-pss-sha256") };
var trust = new[] { new TimeTrustRoot("k1", new byte[32], "ed25519") };
var result = loader.TryLoadHex(hex, TimeTokenFormat.Roughtime, trust, out var anchor);
Assert.True(result.IsValid);
@@ -29,7 +32,7 @@ public class TimeAnchorLoaderTests
[Fact]
public void RejectsIncompatibleTrustRoots()
{
var loader = new TimeAnchorLoader();
var loader = Build();
var hex = "010203";
var rsaKey = new byte[128];
var trust = new[] { new TimeTrustRoot("k1", rsaKey, "rsa") };
@@ -43,10 +46,16 @@ public class TimeAnchorLoaderTests
[Fact]
public void RejectsWhenTrustRootsMissing()
{
var loader = new TimeAnchorLoader();
var loader = Build();
var result = loader.TryLoadHex("010203", TimeTokenFormat.Roughtime, Array.Empty<TimeTrustRoot>(), out _);
Assert.False(result.IsValid);
Assert.Equal("trust-roots-required", result.Reason);
}
private static TimeAnchorLoader Build()
{
var options = Options.Create(new AirGapOptions { AllowUntrustedAnchors = false });
return new TimeAnchorLoader(new TimeVerificationService(), new TimeTokenParser(), options);
}
}

View File

@@ -11,10 +11,14 @@ public class TimeStatusDtoTests
new TimeAnchor(DateTimeOffset.Parse("2025-01-01T00:00:00Z"), "source", "fmt", "fp", "digest"),
new StalenessEvaluation(42, 10, 20, true, false),
new StalenessBudget(10, 20),
new Dictionary<string, StalenessEvaluation>
{
{ "advisories", new StalenessEvaluation(42, 10, 20, true, false) }
},
DateTimeOffset.Parse("2025-01-02T00:00:00Z"));
var json = TimeStatusDto.FromStatus(status).ToJson();
Assert.Equal("{\"anchorTime\":\"2025-01-01T00:00:00.0000000Z\",\"format\":\"fmt\",\"source\":\"source\",\"fingerprint\":\"fp\",\"digest\":\"digest\",\"ageSeconds\":42,\"warningSeconds\":10,\"breachSeconds\":20,\"isWarning\":true,\"isBreach\":false,\"evaluatedAtUtc\":\"2025-01-02T00:00:00.0000000Z\"}", json);
Assert.Contains("\"contentStaleness\":{\"advisories\":{", json);
Assert.Contains("\"ageSeconds\":42", json);
}
}

View File

@@ -9,16 +9,17 @@ public class TimeStatusServiceTests
[Fact]
public async Task ReturnsUnknownWhenNoAnchor()
{
var svc = Build();
var svc = Build(out var telemetry);
var status = await svc.GetStatusAsync("t1", DateTimeOffset.UnixEpoch);
Assert.Equal(TimeAnchor.Unknown, status.Anchor);
Assert.False(status.Staleness.IsWarning);
Assert.Equal(0, telemetry.GetLatest("t1")?.AgeSeconds ?? 0);
}
[Fact]
public async Task PersistsAnchorAndBudget()
{
var svc = Build();
var svc = Build(out var telemetry);
var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "source", "fmt", "fp", "digest");
var budget = new StalenessBudget(10, 20);
@@ -28,7 +29,16 @@ public class TimeStatusServiceTests
Assert.Equal(anchor, status.Anchor);
Assert.True(status.Staleness.IsWarning);
Assert.False(status.Staleness.IsBreach);
var snap = telemetry.GetLatest("t1");
Assert.NotNull(snap);
Assert.Equal(status.Staleness.AgeSeconds, snap!.AgeSeconds);
Assert.True(snap.IsWarning);
}
private static TimeStatusService Build() => new(new InMemoryTimeAnchorStore(), new StalenessCalculator());
private static TimeStatusService Build(out TimeTelemetry telemetry)
{
telemetry = new TimeTelemetry();
var options = Microsoft.Extensions.Options.Options.Create(new AirGapOptions());
return new TimeStatusService(new InMemoryTimeAnchorStore(), new StalenessCalculator(), telemetry, options);
}
}

View File

@@ -0,0 +1,27 @@
using StellaOps.AirGap.Time.Models;
using StellaOps.AirGap.Time.Services;
namespace StellaOps.AirGap.Time.Tests;
public class TimeTelemetryTests
{
[Fact]
public void Records_latest_snapshot_per_tenant()
{
var telemetry = new TimeTelemetry();
var status = new TimeStatus(
new TimeAnchor(DateTimeOffset.UnixEpoch, "src", "fmt", "fp", "digest"),
new StalenessEvaluation(90, 60, 120, true, false),
StalenessBudget.Default,
new Dictionary<string, StalenessEvaluation>{{"advisories", new StalenessEvaluation(90,60,120,true,false)}},
DateTimeOffset.UtcNow);
telemetry.Record("t1", status);
var snap = telemetry.GetLatest("t1");
Assert.NotNull(snap);
Assert.Equal(90, snap!.AgeSeconds);
Assert.True(snap.IsWarning);
Assert.False(snap.IsBreach);
}
}