merge: audit-config query fix + rig live-probe seed (register rows 46, 48) — ValueTuple serialized as {} fixed with AuditLogQueryResult record + ITuple guard; docker/seed-live-probe.sh

This commit is contained in:
Joseph Doherty
2026-08-15 12:00:32 -04:00
7 changed files with 323 additions and 3 deletions
@@ -238,6 +238,85 @@ public class ManagementActorTests : TestKit, IDisposable
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
}
// ========================================================================
// 2b. Configuration-change audit query -- result must not serialize to "{}"
// ========================================================================
/// <summary>
/// Deferred-work register row 46: <c>audit-config query</c> returned the literal
/// <c>{}</c> for every filter combination against the rig even though the
/// <c>AuditLogEntries</c> rows were present. The handler returned the repository's
/// <c>(Entries, TotalCount)</c> ValueTuple straight to <c>SerializeResult</c>, and
/// System.Text.Json writes properties only — a tuple's <c>Item1</c>/<c>Item2</c> are
/// fields, so the whole payload vanished with no error anywhere. Pins that the rows
/// and the total actually reach the wire.
/// </summary>
[Fact]
public void QueryAuditLogCommand_ReturnsEntriesAndTotalCount()
{
var uiRepo = Substitute.For<ICentralUiRepository>();
var entries = new List<Commons.Entities.Audit.AuditLogEntry>
{
new("alice", "Created", "Template", "7", "PumpTemplate")
{
Id = 1,
Timestamp = new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.Zero)
}
};
uiRepo.GetAuditLogEntriesAsync(
Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<string?>(),
Arg.Any<DateTimeOffset?>(), Arg.Any<DateTimeOffset?>(),
Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<Guid?>(),
Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(((IReadOnlyList<Commons.Entities.Audit.AuditLogEntry>)entries, 137));
_services.AddScoped(_ => uiRepo);
var actor = CreateActor();
var envelope = Envelope(new QueryAuditLogCommand(PageSize: 25), "Administrator");
actor.Tell(envelope);
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
Assert.NotEqual("{}", response.JsonData);
Assert.Contains("PumpTemplate", response.JsonData);
Assert.Contains("\"entries\"", response.JsonData);
Assert.Contains("\"totalCount\":137", response.JsonData);
}
/// <summary>
/// Guard for the whole class of bug behind row 46: any handler that returns a tuple
/// silently serializes to <c>{}</c>. <see cref="ManagementActor.SerializeResult"/>
/// now rejects tuples outright so the next occurrence is a loud failure rather than
/// an empty response.
/// </summary>
[Fact]
public void SerializeResult_TupleResult_ThrowsInsteadOfEmittingEmptyObject()
{
var ex = Assert.Throws<InvalidOperationException>(
() => ManagementActor.SerializeResult((new[] { "a" }, 1)));
Assert.Contains("tuple", ex.Message, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Pins the positive half of the guard: the named result record the handler now
/// returns serializes to a populated, camelCased object.
/// </summary>
[Fact]
public void SerializeResult_AuditLogQueryResult_EmitsEntriesAndTotalCount()
{
var json = ManagementActor.SerializeResult(new AuditLogQueryResult(
new List<Commons.Entities.Audit.AuditLogEntry>
{
new("bob", "Deleted", "Instance", "3", "Pump1")
},
42));
Assert.Contains("\"entries\"", json);
Assert.Contains("\"totalCount\":42", json);
Assert.Contains("Pump1", json);
}
// ========================================================================
// 3. Template list test -- verify data flows through
// ========================================================================