sprints work
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
// <copyright file="AgentRegistrationServiceTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AgentRegistrationService"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class AgentRegistrationServiceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly AgentRegistrationService _service;
|
||||
|
||||
public AgentRegistrationServiceTests()
|
||||
{
|
||||
_service = new AgentRegistrationService(
|
||||
_timeProvider,
|
||||
NullLogger<AgentRegistrationService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAsync_ValidRequest_ReturnsRegistration()
|
||||
{
|
||||
var request = CreateRegistrationRequest("agent-1");
|
||||
|
||||
var result = await _service.RegisterAsync(request);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.AgentId.Should().Be("agent-1");
|
||||
result.Platform.Should().Be(RuntimePlatform.DotNet);
|
||||
result.Hostname.Should().Be("host1");
|
||||
result.State.Should().Be(AgentState.Stopped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAsync_DuplicateAgent_UpdatesRegistration()
|
||||
{
|
||||
var request1 = CreateRegistrationRequest("agent-1");
|
||||
var request2 = CreateRegistrationRequest("agent-1") with { ApplicationName = "App2" };
|
||||
|
||||
await _service.RegisterAsync(request1);
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
var result = await _service.RegisterAsync(request2);
|
||||
|
||||
result.ApplicationName.Should().Be("App2");
|
||||
_service.Count.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeartbeatAsync_RegisteredAgent_UpdatesState()
|
||||
{
|
||||
var request = CreateRegistrationRequest("agent-1");
|
||||
await _service.RegisterAsync(request);
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(30));
|
||||
var heartbeat = new AgentHeartbeatRequest
|
||||
{
|
||||
AgentId = "agent-1",
|
||||
State = AgentState.Running,
|
||||
Posture = RuntimePosture.Full,
|
||||
Statistics = CreateStatistics("agent-1", 1000)
|
||||
};
|
||||
|
||||
var response = await _service.HeartbeatAsync(heartbeat);
|
||||
|
||||
response.Continue.Should().BeTrue();
|
||||
var registration = await _service.GetAsync("agent-1");
|
||||
registration!.State.Should().Be(AgentState.Running);
|
||||
registration.Posture.Should().Be(RuntimePosture.Full);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeartbeatAsync_UnknownAgent_ReturnsContinueFalse()
|
||||
{
|
||||
var heartbeat = new AgentHeartbeatRequest
|
||||
{
|
||||
AgentId = "unknown-agent",
|
||||
State = AgentState.Running,
|
||||
Posture = RuntimePosture.Sampled
|
||||
};
|
||||
|
||||
var response = await _service.HeartbeatAsync(heartbeat);
|
||||
|
||||
response.Continue.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeartbeatAsync_WithPendingCommand_ReturnsCommand()
|
||||
{
|
||||
var request = CreateRegistrationRequest("agent-1");
|
||||
await _service.RegisterAsync(request);
|
||||
await _service.SendCommandAsync("agent-1", AgentCommand.Start);
|
||||
|
||||
var heartbeat = new AgentHeartbeatRequest
|
||||
{
|
||||
AgentId = "agent-1",
|
||||
State = AgentState.Stopped,
|
||||
Posture = RuntimePosture.Sampled
|
||||
};
|
||||
var response = await _service.HeartbeatAsync(heartbeat);
|
||||
|
||||
response.Command.Should().Be(AgentCommand.Start);
|
||||
|
||||
// Second heartbeat should not have command
|
||||
var response2 = await _service.HeartbeatAsync(heartbeat);
|
||||
response2.Command.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeartbeatAsync_WithPendingPosture_ReturnsNewPosture()
|
||||
{
|
||||
var request = CreateRegistrationRequest("agent-1");
|
||||
await _service.RegisterAsync(request);
|
||||
await _service.UpdatePostureAsync("agent-1", RuntimePosture.Full);
|
||||
|
||||
var heartbeat = new AgentHeartbeatRequest
|
||||
{
|
||||
AgentId = "agent-1",
|
||||
State = AgentState.Running,
|
||||
Posture = RuntimePosture.Sampled
|
||||
};
|
||||
var response = await _service.HeartbeatAsync(heartbeat);
|
||||
|
||||
response.NewPosture.Should().Be(RuntimePosture.Full);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnregisterAsync_RegisteredAgent_RemovesFromList()
|
||||
{
|
||||
var request = CreateRegistrationRequest("agent-1");
|
||||
await _service.RegisterAsync(request);
|
||||
|
||||
await _service.UnregisterAsync("agent-1");
|
||||
|
||||
var result = await _service.GetAsync("agent-1");
|
||||
result.Should().BeNull();
|
||||
_service.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListAsync_ReturnsAllRegistrations()
|
||||
{
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-1"));
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-2"));
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-3"));
|
||||
|
||||
var result = await _service.ListAsync();
|
||||
|
||||
result.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListByPlatformAsync_FiltersCorrectly()
|
||||
{
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-1") with
|
||||
{
|
||||
Platform = RuntimePlatform.DotNet
|
||||
});
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-2") with
|
||||
{
|
||||
Platform = RuntimePlatform.Java
|
||||
});
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-3") with
|
||||
{
|
||||
Platform = RuntimePlatform.DotNet
|
||||
});
|
||||
|
||||
var result = await _service.ListByPlatformAsync(RuntimePlatform.DotNet);
|
||||
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().AllSatisfy(r => r.Platform.Should().Be(RuntimePlatform.DotNet));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListHealthyAsync_FiltersStaleAgents()
|
||||
{
|
||||
_service.HeartbeatTimeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-1"));
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-2"));
|
||||
|
||||
// Advance time and only heartbeat agent-1
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1));
|
||||
await _service.HeartbeatAsync(new AgentHeartbeatRequest
|
||||
{
|
||||
AgentId = "agent-1",
|
||||
State = AgentState.Running,
|
||||
Posture = RuntimePosture.Sampled
|
||||
});
|
||||
|
||||
// Advance 30 seconds - agent-1 should still be healthy (1.5 min since heartbeat)
|
||||
// but agent-2 is unhealthy (2.5 min since registration/initial heartbeat)
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(1.5));
|
||||
|
||||
var healthy = await _service.ListHealthyAsync();
|
||||
|
||||
healthy.Should().HaveCount(1);
|
||||
healthy[0].AgentId.Should().Be("agent-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PruneStale_RemovesExpiredRegistrations()
|
||||
{
|
||||
_service.HeartbeatTimeout = TimeSpan.FromMinutes(2);
|
||||
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-1"));
|
||||
await _service.RegisterAsync(CreateRegistrationRequest("agent-2"));
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(3));
|
||||
|
||||
var pruned = _service.PruneStale();
|
||||
|
||||
pruned.Should().Be(2);
|
||||
_service.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendCommandAsync_UnknownAgent_DoesNotThrow()
|
||||
{
|
||||
await _service.SendCommandAsync("unknown", AgentCommand.Start);
|
||||
|
||||
// Should not throw, just log warning
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdatePostureAsync_UnknownAgent_DoesNotThrow()
|
||||
{
|
||||
await _service.UpdatePostureAsync("unknown", RuntimePosture.Full);
|
||||
|
||||
// Should not throw, just log warning
|
||||
}
|
||||
|
||||
private static AgentRegistrationRequest CreateRegistrationRequest(string agentId)
|
||||
{
|
||||
return new AgentRegistrationRequest
|
||||
{
|
||||
AgentId = agentId,
|
||||
Platform = RuntimePlatform.DotNet,
|
||||
Hostname = "host1",
|
||||
AgentVersion = "1.0.0",
|
||||
ApplicationName = "TestApp",
|
||||
InitialPosture = RuntimePosture.Sampled
|
||||
};
|
||||
}
|
||||
|
||||
private AgentStatistics CreateStatistics(string agentId, long eventsCollected)
|
||||
{
|
||||
return new AgentStatistics
|
||||
{
|
||||
AgentId = agentId,
|
||||
Timestamp = _timeProvider.GetUtcNow(),
|
||||
State = AgentState.Running,
|
||||
Uptime = TimeSpan.FromMinutes(5),
|
||||
TotalEventsCollected = eventsCollected,
|
||||
EventsLastMinute = Math.Min(eventsCollected, 1000),
|
||||
EventsDropped = 0,
|
||||
UniqueMethodsObserved = (int)(eventsCollected / 10),
|
||||
UniqueTypesObserved = (int)(eventsCollected / 100),
|
||||
UniqueAssembliesObserved = 5,
|
||||
BufferUtilizationPercent = 25.0,
|
||||
EstimatedCpuOverheadPercent = 1.5,
|
||||
MemoryUsageBytes = 50_000_000
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// <copyright file="ClrMethodResolverTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ClrMethodResolver"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class ClrMethodResolverTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly ClrMethodResolver _resolver;
|
||||
|
||||
public ClrMethodResolverTests()
|
||||
{
|
||||
_resolver = new ClrMethodResolver(
|
||||
_timeProvider,
|
||||
NullLogger<ClrMethodResolver>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterMethod_IncrementsCacheCount()
|
||||
{
|
||||
_resolver.RegisterMethod(
|
||||
methodId: 0x06000001,
|
||||
moduleId: 0x00007FF8ABC12340,
|
||||
methodNamespace: "MyApp.Services",
|
||||
methodName: "ProcessData",
|
||||
methodSignature: "(System.String)");
|
||||
|
||||
_resolver.CachedMethodCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterModule_IncrementsCacheCount()
|
||||
{
|
||||
_resolver.RegisterModule(
|
||||
moduleId: 0x00007FF8ABC12340,
|
||||
assemblyId: 0x00007FF8DEF00000,
|
||||
modulePath: @"C:\app\MyApp.dll",
|
||||
simpleName: "MyApp");
|
||||
|
||||
_resolver.CachedModuleCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveMethod_RegisteredMethod_ReturnsResolved()
|
||||
{
|
||||
const ulong methodId = 0x06000001;
|
||||
_resolver.RegisterMethod(
|
||||
methodId: methodId,
|
||||
moduleId: 0x00007FF8ABC12340,
|
||||
methodNamespace: "MyApp.Services.DataService",
|
||||
methodName: "ProcessData",
|
||||
methodSignature: "(System.String)");
|
||||
|
||||
var resolved = _resolver.ResolveMethod(methodId);
|
||||
|
||||
resolved.Should().NotBeNull();
|
||||
resolved!.MethodName.Should().Be("ProcessData");
|
||||
resolved.Namespace.Should().Be("MyApp.Services.DataService");
|
||||
resolved.TypeName.Should().Be("DataService");
|
||||
resolved.Signature.Should().Be("(System.String)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveMethod_UnknownMethod_ReturnsNull()
|
||||
{
|
||||
var resolved = _resolver.ResolveMethod(0xDEADBEEF);
|
||||
|
||||
resolved.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveToEvent_RegisteredMethod_ReturnsEvent()
|
||||
{
|
||||
const ulong methodId = 0x06000001;
|
||||
const ulong moduleId = 0x00007FF8ABC12340;
|
||||
|
||||
_resolver.RegisterModule(moduleId, 0, @"C:\app\MyApp.dll", "MyApp");
|
||||
_resolver.RegisterMethod(
|
||||
methodId: methodId,
|
||||
moduleId: moduleId,
|
||||
methodNamespace: "MyApp.Services.DataService",
|
||||
methodName: "ProcessData",
|
||||
methodSignature: "(System.String)");
|
||||
|
||||
var @event = _resolver.ResolveToEvent(
|
||||
methodId,
|
||||
RuntimeEventKind.MethodEnter,
|
||||
eventId: "test-event-1",
|
||||
timestamp: _timeProvider.GetUtcNow(),
|
||||
processId: 1234);
|
||||
|
||||
@event.Should().NotBeNull();
|
||||
@event!.MethodName.Should().Be("ProcessData");
|
||||
@event.TypeName.Should().Be("DataService");
|
||||
@event.AssemblyOrModule.Should().Be("MyApp");
|
||||
@event.Kind.Should().Be(RuntimeEventKind.MethodEnter);
|
||||
@event.Platform.Should().Be(RuntimePlatform.DotNet);
|
||||
@event.ProcessId.Should().Be(1234);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveToEvent_UnknownMethod_ReturnsNull()
|
||||
{
|
||||
var @event = _resolver.ResolveToEvent(
|
||||
0xDEADBEEF,
|
||||
RuntimeEventKind.MethodEnter,
|
||||
eventId: "test-event-1",
|
||||
timestamp: _timeProvider.GetUtcNow());
|
||||
|
||||
@event.Should().BeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("MethodID=0x06000123", 0x06000123UL)]
|
||||
[InlineData("MethodID=0x00000001", 0x00000001UL)]
|
||||
[InlineData("MethodID=0xDEADBEEF", 0xDEADBEEFUL)]
|
||||
public void TryParseEtwMethodId_ValidInput_ReturnsMethodId(string input, ulong expected)
|
||||
{
|
||||
var success = _resolver.TryParseEtwMethodId(input, out var methodId);
|
||||
|
||||
success.Should().BeTrue();
|
||||
methodId.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("invalid")]
|
||||
[InlineData("MethodID=invalid")]
|
||||
[InlineData("ModuleID=0x123")]
|
||||
public void TryParseEtwMethodId_InvalidInput_ReturnsFalse(string input)
|
||||
{
|
||||
var success = _resolver.TryParseEtwMethodId(input, out _);
|
||||
|
||||
success.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEtwMethodWithModule_ValidInput_ReturnsBoth()
|
||||
{
|
||||
var result = _resolver.ParseEtwMethodWithModule(
|
||||
"MethodID=0x06000123 ModuleID=0x00007FF8ABC12340");
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Value.MethodId.Should().Be(0x06000123UL);
|
||||
result.Value.ModuleId.Should().Be(0x00007FF8ABC12340UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEtwMethodWithModule_MethodOnly_ReturnsZeroModule()
|
||||
{
|
||||
var result = _resolver.ParseEtwMethodWithModule("MethodID=0x06000123");
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result!.Value.MethodId.Should().Be(0x06000123UL);
|
||||
result.Value.ModuleId.Should().Be(0UL);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatSymbolId_ReturnsCorrectFormat()
|
||||
{
|
||||
var symbolId = ClrMethodResolver.FormatSymbolId(0x06000123);
|
||||
|
||||
symbolId.Should().Be("clr:method:0000000006000123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_RemovesAllCaches()
|
||||
{
|
||||
_resolver.RegisterMethod(0x1, 0x2, "ns", "method", "()");
|
||||
_resolver.RegisterModule(0x2, 0x3, "path", "name");
|
||||
_resolver.RegisterAssembly(0x3, "assembly");
|
||||
|
||||
_resolver.Clear();
|
||||
|
||||
_resolver.CachedMethodCount.Should().Be(0);
|
||||
_resolver.CachedModuleCount.Should().Be(0);
|
||||
_resolver.GetStatistics().CachedAssemblies.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStatistics_ReturnsCorrectCounts()
|
||||
{
|
||||
_resolver.RegisterMethod(0x1, 0x2, "ns1", "method1", "()");
|
||||
_resolver.RegisterMethod(0x2, 0x2, "ns2", "method2", "()");
|
||||
_resolver.RegisterModule(0x10, 0x20, "path1", "name1");
|
||||
_resolver.RegisterAssembly(0x20, "assembly1");
|
||||
|
||||
var stats = _resolver.GetStatistics();
|
||||
|
||||
stats.CachedMethods.Should().Be(2);
|
||||
stats.CachedModules.Should().Be(1);
|
||||
stats.CachedAssemblies.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvedMethod_FullyQualifiedName_CombinesCorrectly()
|
||||
{
|
||||
const ulong methodId = 0x06000001;
|
||||
_resolver.RegisterMethod(
|
||||
methodId: methodId,
|
||||
moduleId: 0,
|
||||
methodNamespace: "MyApp.Services.DataService",
|
||||
methodName: "ProcessData",
|
||||
methodSignature: "()");
|
||||
|
||||
var resolved = _resolver.ResolveMethod(methodId);
|
||||
|
||||
resolved!.FullyQualifiedName.Should().Be("MyApp.Services.DataService.ProcessData");
|
||||
resolved.DisplayName.Should().Be("MyApp.Services.DataService.ProcessData()");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolvedMethod_EmptyNamespace_UsesMethodNameOnly()
|
||||
{
|
||||
const ulong methodId = 0x06000001;
|
||||
_resolver.RegisterMethod(
|
||||
methodId: methodId,
|
||||
moduleId: 0,
|
||||
methodNamespace: "",
|
||||
methodName: "Main",
|
||||
methodSignature: "()");
|
||||
|
||||
var resolved = _resolver.ResolveMethod(methodId);
|
||||
|
||||
resolved!.FullyQualifiedName.Should().Be("Main");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegisterAssembly_ResolvesInEvent()
|
||||
{
|
||||
const ulong methodId = 0x06000001;
|
||||
const ulong moduleId = 0x00007FF8ABC12340;
|
||||
const ulong assemblyId = 0x00007FF8DEF00000;
|
||||
|
||||
_resolver.RegisterAssembly(assemblyId, "MyApp.Core, Version=1.0.0.0");
|
||||
_resolver.RegisterModule(moduleId, assemblyId, @"C:\app\MyApp.Core.dll", "MyApp.Core");
|
||||
_resolver.RegisterMethod(methodId, moduleId, "MyApp.Core.Data", "Load", "()");
|
||||
|
||||
var @event = _resolver.ResolveToEvent(
|
||||
methodId,
|
||||
RuntimeEventKind.MethodEnter,
|
||||
eventId: "e1",
|
||||
timestamp: _timeProvider.GetUtcNow());
|
||||
|
||||
@event!.AssemblyOrModule.Should().Be("MyApp.Core, Version=1.0.0.0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// <copyright file="RuntimeFactsIngestServiceTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RuntimeFactsIngestService"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class RuntimeFactsIngestServiceTests : IAsyncLifetime
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly AgentRegistrationService _registrationService;
|
||||
private readonly RuntimeFactsIngestService _service;
|
||||
|
||||
public RuntimeFactsIngestServiceTests()
|
||||
{
|
||||
_registrationService = new AgentRegistrationService(
|
||||
_timeProvider,
|
||||
NullLogger<AgentRegistrationService>.Instance);
|
||||
|
||||
_service = new RuntimeFactsIngestService(
|
||||
_timeProvider,
|
||||
_registrationService,
|
||||
NullLogger<RuntimeFactsIngestService>.Instance);
|
||||
}
|
||||
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _service.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestAsync_EmptyEvents_ReturnsZero()
|
||||
{
|
||||
var count = await _service.IngestAsync("agent-1", [], CancellationToken.None);
|
||||
|
||||
count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestAsync_ValidEvents_ReturnsCount()
|
||||
{
|
||||
var events = CreateEvents(5);
|
||||
|
||||
var count = await _service.IngestAsync("agent-1", events, CancellationToken.None);
|
||||
|
||||
count.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestAsync_ProcessesEventsThroughChannel()
|
||||
{
|
||||
var events = CreateEvents(10);
|
||||
|
||||
await _service.IngestAsync("agent-1", events, CancellationToken.None);
|
||||
|
||||
// Allow time for background processing
|
||||
await Task.Delay(100);
|
||||
|
||||
var stats = _service.GetStatistics();
|
||||
stats.TotalEventsIngested.Should().Be(10);
|
||||
stats.TotalBatchesProcessed.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestAsync_AggregatesSymbolObservations()
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var events = new List<RuntimeMethodEvent>
|
||||
{
|
||||
CreateEvent("symbol-1", "Method1", now),
|
||||
CreateEvent("symbol-1", "Method1", now.AddSeconds(1)),
|
||||
CreateEvent("symbol-1", "Method1", now.AddSeconds(2)),
|
||||
CreateEvent("symbol-2", "Method2", now)
|
||||
};
|
||||
|
||||
await _service.IngestAsync("agent-1", events, CancellationToken.None);
|
||||
await Task.Delay(100);
|
||||
|
||||
var observation = _service.GetObservation("symbol-1");
|
||||
observation.Should().NotBeNull();
|
||||
observation!.ObservationCount.Should().Be(3);
|
||||
observation.MethodName.Should().Be("Method1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IngestAsync_TracksMultipleAgents()
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var event1 = CreateEvent("symbol-1", "Method1", now);
|
||||
var event2 = CreateEvent("symbol-1", "Method1", now.AddSeconds(1));
|
||||
|
||||
await _service.IngestAsync("agent-1", [event1], CancellationToken.None);
|
||||
await _service.IngestAsync("agent-2", [event2], CancellationToken.None);
|
||||
await Task.Delay(100);
|
||||
|
||||
var observation = _service.GetObservation("symbol-1");
|
||||
observation!.AgentIds.Should().Contain("agent-1");
|
||||
observation.AgentIds.Should().Contain("agent-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetObservedSymbols_ReturnsAllUniqueSymbols()
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var events = new List<RuntimeMethodEvent>
|
||||
{
|
||||
CreateEvent("symbol-1", "Method1", now),
|
||||
CreateEvent("symbol-2", "Method2", now),
|
||||
CreateEvent("symbol-3", "Method3", now),
|
||||
CreateEvent("symbol-1", "Method1", now) // Duplicate
|
||||
};
|
||||
|
||||
await _service.IngestAsync("agent-1", events, CancellationToken.None);
|
||||
await Task.Delay(100);
|
||||
|
||||
var symbols = _service.GetObservedSymbols();
|
||||
symbols.Should().HaveCount(3);
|
||||
symbols.Should().Contain("symbol-1");
|
||||
symbols.Should().Contain("symbol-2");
|
||||
symbols.Should().Contain("symbol-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetObservationsSince_FiltersCorrectly()
|
||||
{
|
||||
var baseTime = _timeProvider.GetUtcNow();
|
||||
var events1 = new List<RuntimeMethodEvent>
|
||||
{
|
||||
CreateEvent("symbol-1", "Method1", baseTime)
|
||||
};
|
||||
|
||||
await _service.IngestAsync("agent-1", events1, CancellationToken.None);
|
||||
await Task.Delay(100);
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromHours(1));
|
||||
var laterTime = _timeProvider.GetUtcNow();
|
||||
|
||||
var events2 = new List<RuntimeMethodEvent>
|
||||
{
|
||||
CreateEvent("symbol-2", "Method2", laterTime)
|
||||
};
|
||||
|
||||
await _service.IngestAsync("agent-1", events2, CancellationToken.None);
|
||||
await Task.Delay(100);
|
||||
|
||||
var recentObservations = _service.GetObservationsSince(baseTime.AddMinutes(30));
|
||||
recentObservations.Should().HaveCount(1);
|
||||
recentObservations[0].SymbolId.Should().Be("symbol-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatistics_ReturnsCorrectCounts()
|
||||
{
|
||||
var events1 = CreateEvents(5);
|
||||
var events2 = CreateEvents(3);
|
||||
|
||||
await _service.IngestAsync("agent-1", events1, CancellationToken.None);
|
||||
await _service.IngestAsync("agent-1", events2, CancellationToken.None);
|
||||
await Task.Delay(100);
|
||||
|
||||
var stats = _service.GetStatistics();
|
||||
stats.TotalEventsIngested.Should().Be(8);
|
||||
stats.TotalBatchesProcessed.Should().Be(2);
|
||||
stats.UniqueSymbolsObserved.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RegisterAgentAsync_DoesNotThrow()
|
||||
{
|
||||
var registration = new AgentRegistration
|
||||
{
|
||||
AgentId = "agent-1",
|
||||
Platform = RuntimePlatform.DotNet,
|
||||
Hostname = "host1",
|
||||
AgentVersion = "1.0.0",
|
||||
RegisteredAt = _timeProvider.GetUtcNow(),
|
||||
LastHeartbeat = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
await _service.RegisterAgentAsync(registration, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HeartbeatAsync_DoesNotThrow()
|
||||
{
|
||||
var stats = CreateStatistics("agent-1", 100);
|
||||
await _service.HeartbeatAsync("agent-1", stats, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnregisterAgentAsync_DoesNotThrow()
|
||||
{
|
||||
await _service.UnregisterAgentAsync("agent-1", CancellationToken.None);
|
||||
}
|
||||
|
||||
private List<RuntimeMethodEvent> CreateEvents(int count)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
return Enumerable.Range(0, count)
|
||||
.Select(i => CreateEvent($"symbol-{i}", $"Method{i}", now.AddMilliseconds(i)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private AgentStatistics CreateStatistics(string agentId, long eventsCollected)
|
||||
{
|
||||
return new AgentStatistics
|
||||
{
|
||||
AgentId = agentId,
|
||||
Timestamp = _timeProvider.GetUtcNow(),
|
||||
State = AgentState.Running,
|
||||
Uptime = TimeSpan.FromMinutes(5),
|
||||
TotalEventsCollected = eventsCollected,
|
||||
EventsLastMinute = Math.Min(eventsCollected, 1000),
|
||||
EventsDropped = 0,
|
||||
UniqueMethodsObserved = (int)(eventsCollected / 10),
|
||||
UniqueTypesObserved = (int)(eventsCollected / 100),
|
||||
UniqueAssembliesObserved = 5,
|
||||
BufferUtilizationPercent = 25.0,
|
||||
EstimatedCpuOverheadPercent = 1.5,
|
||||
MemoryUsageBytes = 50_000_000
|
||||
};
|
||||
}
|
||||
|
||||
private static RuntimeMethodEvent CreateEvent(string symbolId, string methodName, DateTimeOffset timestamp)
|
||||
{
|
||||
return new RuntimeMethodEvent
|
||||
{
|
||||
EventId = Guid.NewGuid().ToString("N"),
|
||||
SymbolId = symbolId,
|
||||
MethodName = methodName,
|
||||
TypeName = "TestType",
|
||||
AssemblyOrModule = "TestAssembly",
|
||||
Timestamp = timestamp,
|
||||
Kind = RuntimeEventKind.MethodEnter,
|
||||
Platform = RuntimePlatform.DotNet
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user