feat(management): opportunistic expiry sweep on secured-write list (arch-review S2)

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 05:39:29 -04:00
parent 064a7d9415
commit 03295e91bb
3 changed files with 53 additions and 1 deletions
@@ -613,6 +613,42 @@ public class SecuredWriteHandlerTests : TestKit, IDisposable
Assert.Contains("not found", updated.ExecutionError);
}
// ------------------------------------------------------------------------
// Opportunistic expiry sweep on list — Task 16 (arch-review S2)
// ------------------------------------------------------------------------
[Fact]
public void List_SweepsStalePendingRows_MarksExpired_CallsTryMarkExpiredOnceForStaleOnly()
{
var fresh = new PendingSecuredWrite
{
Id = 1, SiteId = "SITE1", ConnectionName = "Mx1", TagPath = "Tag.A",
ValueJson = "true", ValueType = "Boolean", Status = "Pending",
OperatorUser = "alice", SubmittedAtUtc = DateTime.UtcNow.AddHours(-1)
};
var stale = new PendingSecuredWrite
{
Id = 2, SiteId = "SITE1", ConnectionName = "Mx1", TagPath = "Tag.B",
ValueJson = "false", ValueType = "Boolean", Status = "Pending",
OperatorUser = "alice", SubmittedAtUtc = DateTime.UtcNow.AddHours(-25) // TTL default 24h
};
_securedWriteRepo.QueryAsync(null, null, 0, 200, Arg.Any<CancellationToken>())
.Returns(new List<PendingSecuredWrite> { fresh, stale });
_securedWriteRepo.TryMarkExpiredAsync(2, Arg.Any<DateTime>(), Arg.Any<CancellationToken>())
.Returns(true);
var actor = CreateActor();
actor.Tell(Envelope(new ListSecuredWritesCommand(null, null), "carol", "Operator"));
var resp = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
// The stale row surfaces as Expired in the projection; the fresh row stays Pending.
Assert.Contains("\"id\":2", resp.JsonData);
Assert.Contains("Expired", resp.JsonData);
// Exactly one CAS — only the overdue row.
_securedWriteRepo.Received(1).TryMarkExpiredAsync(2, Arg.Any<DateTime>(), Arg.Any<CancellationToken>());
_securedWriteRepo.DidNotReceive().TryMarkExpiredAsync(1, Arg.Any<DateTime>(), Arg.Any<CancellationToken>());
}
// ------------------------------------------------------------------------
// Server-side TTL expiry — Task 15 (arch-review S2)
// ------------------------------------------------------------------------