feat(sitecallaudit): central→site Retry/Discard relay for parked operations

This commit is contained in:
Joseph Doherty
2026-05-21 04:36:04 -04:00
parent ac1f73cf8a
commit 7816b840c1
13 changed files with 1025 additions and 1 deletions

View File

@@ -382,6 +382,64 @@ public class CommunicationServiceTests : TestKit
Assert.Equal("plant-a", result.Sites[0].SourceSite);
}
[Fact]
public async Task RetrySiteCallAsync_BeforeSiteCallAuditSet_Throws()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
service.RetrySiteCallAsync(new RetrySiteCallRequest("corr-1", Guid.NewGuid(), "plant-a")));
}
[Fact]
public async Task RetrySiteCallAsync_AsksSiteCallAuditProxyDirectly()
{
// The relay is initiated by Asking the central-local Site Call Audit
// proxy directly (no SiteEnvelope wrapping at this layer — the actor
// does the site routing itself).
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
var probe = CreateTestProbe();
service.SetSiteCallAudit(probe.Ref);
var request = new RetrySiteCallRequest("corr-r", Guid.NewGuid(), "plant-a");
var task = service.RetrySiteCallAsync(request);
var received = probe.ExpectMsg<RetrySiteCallRequest>();
Assert.Same(request, received);
var reply = new RetrySiteCallResponse(
"corr-r", SiteCallRelayOutcome.Applied, true, true, null);
probe.Reply(reply);
Assert.Same(reply, await task);
}
[Fact]
public async Task DiscardSiteCallAsync_AsksSiteCallAuditProxyDirectly()
{
var service = new CommunicationService(
Options.Create(new CommunicationOptions()),
NullLogger<CommunicationService>.Instance);
var probe = CreateTestProbe();
service.SetSiteCallAudit(probe.Ref);
var request = new DiscardSiteCallRequest("corr-d", Guid.NewGuid(), "plant-a");
var task = service.DiscardSiteCallAsync(request);
var received = probe.ExpectMsg<DiscardSiteCallRequest>();
Assert.Same(request, received);
var reply = new DiscardSiteCallResponse(
"corr-d", SiteCallRelayOutcome.SiteUnreachable, false, false, "unreachable");
probe.Reply(reply);
var result = await task;
Assert.Same(reply, result);
Assert.False(result.SiteReachable);
}
/// <summary>
/// Stand-in for CentralCommunicationActor: verifies the message is wrapped
/// in a SiteEnvelope targeting the requested site and replies with a typed

View File

@@ -214,4 +214,72 @@ public class SiteCommunicationActorTests : TestKit
ExpectMsg<EventLogQueryResponse>(msg => !msg.Success);
}
// ── Task 5 (#22): central→site Retry/Discard relay for parked cached calls ──
[Fact]
public void RetryParkedOperation_WithHandler_ForwardedToParkedMessageHandler()
{
var dmProbe = CreateTestProbe();
var handlerProbe = CreateTestProbe();
var siteActor = Sys.ActorOf(Props.Create(() =>
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
siteActor.Tell(new RegisterLocalHandler(LocalHandlerType.ParkedMessages, handlerProbe.Ref));
var id = Commons.Types.TrackedOperationId.New();
siteActor.Tell(new RetryParkedOperation("corr-rp", id));
handlerProbe.ExpectMsg<RetryParkedOperation>(msg =>
msg.CorrelationId == "corr-rp" && msg.TrackedOperationId.Equals(id));
}
[Fact]
public void DiscardParkedOperation_WithHandler_ForwardedToParkedMessageHandler()
{
var dmProbe = CreateTestProbe();
var handlerProbe = CreateTestProbe();
var siteActor = Sys.ActorOf(Props.Create(() =>
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
siteActor.Tell(new RegisterLocalHandler(LocalHandlerType.ParkedMessages, handlerProbe.Ref));
var id = Commons.Types.TrackedOperationId.New();
siteActor.Tell(new DiscardParkedOperation("corr-dp", id));
handlerProbe.ExpectMsg<DiscardParkedOperation>(msg =>
msg.CorrelationId == "corr-dp" && msg.TrackedOperationId.Equals(id));
}
[Fact]
public void RetryParkedOperation_WithoutHandler_RepliesNotAppliedAck()
{
// No parked-message handler registered — the relay must get a definitive
// non-applied ack, not silence (the SiteCallAuditActor's Ask must not
// hang and then mis-report site-unreachable when the site IS reachable).
var dmProbe = CreateTestProbe();
var siteActor = Sys.ActorOf(Props.Create(() =>
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
siteActor.Tell(new RetryParkedOperation("corr-no-handler", Commons.Types.TrackedOperationId.New()));
var ack = ExpectMsg<ParkedOperationActionAck>();
Assert.Equal("corr-no-handler", ack.CorrelationId);
Assert.False(ack.Applied);
Assert.NotNull(ack.ErrorMessage);
}
[Fact]
public void DiscardParkedOperation_WithoutHandler_RepliesNotAppliedAck()
{
var dmProbe = CreateTestProbe();
var siteActor = Sys.ActorOf(Props.Create(() =>
new SiteCommunicationActor("site1", _options, dmProbe.Ref)));
siteActor.Tell(new DiscardParkedOperation("corr-no-handler", Commons.Types.TrackedOperationId.New()));
var ack = ExpectMsg<ParkedOperationActionAck>();
Assert.False(ack.Applied);
Assert.NotNull(ack.ErrorMessage);
}
}