test(auditlog): assert ExecutionId threading hops; defensive Guid parse on S&F read

This commit is contained in:
Joseph Doherty
2026-05-21 15:27:37 -04:00
parent 6f5a35f222
commit 705ae95404
6 changed files with 195 additions and 17 deletions

View File

@@ -155,6 +155,46 @@ public class DatabaseCachedWriteEmissionTests
Times.Once);
}
/// <summary>
/// Audit Log #23 (ExecutionId Task 4): the helper → gateway hop of the
/// threading chain. The cached-write helper must forward the runtime
/// context's <c>ExecutionId</c> and <c>SourceScript</c> verbatim into
/// <see cref="IDatabaseGateway.CachedWriteAsync"/> — so the buffered retry
/// loop later stamps the right provenance onto its audit rows. This
/// asserts the exact id/script (not <c>It.IsAny</c>), so a regression that
/// dropped the threading would fail here.
/// </summary>
[Fact]
public async Task CachedWrite_ThreadsExecutionIdAndSourceScript_IntoGateway()
{
var gateway = new Mock<IDatabaseGateway>();
gateway
.Setup(g => g.CachedWriteAsync(
"myDb", "INSERT INTO t VALUES (1)",
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
InstanceName,
It.IsAny<CancellationToken>(),
It.IsAny<TrackedOperationId?>(),
It.IsAny<Guid?>(), It.IsAny<string?>()))
.Returns(Task.CompletedTask);
var forwarder = new CapturingForwarder();
var helper = CreateHelper(gateway.Object, forwarder);
await helper.CachedWrite("myDb", "INSERT INTO t VALUES (1)");
// The known TestExecutionId and SourceScript must reach the gateway
// unchanged — these are what the S&F retry loop persists and replays.
gateway.Verify(g => g.CachedWriteAsync(
"myDb", "INSERT INTO t VALUES (1)",
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
InstanceName,
It.IsAny<CancellationToken>(),
It.IsAny<TrackedOperationId?>(),
It.Is<Guid?>(id => id == TestExecutionId),
It.Is<string?>(s => s == SourceScript)),
Times.Once);
}
[Fact]
public async Task CachedWrite_ForwarderThrows_StillReturnsTrackedOperationId()
{

View File

@@ -188,6 +188,46 @@ public class ExternalSystemCachedCallEmissionTests
Times.Once);
}
/// <summary>
/// Audit Log #23 (ExecutionId Task 4): the helper → gateway hop of the
/// threading chain. The cached-call helper must forward the runtime
/// context's <c>ExecutionId</c> and <c>SourceScript</c> verbatim into
/// <see cref="IExternalSystemClient.CachedCallAsync"/> — so the buffered
/// retry loop later stamps the right provenance onto its audit rows.
/// This asserts the exact id/script (not <c>It.IsAny</c>), so a regression
/// that dropped the threading would fail here.
/// </summary>
[Fact]
public async Task CachedCall_ThreadsExecutionIdAndSourceScript_IntoClient()
{
var client = new Mock<IExternalSystemClient>();
client
.Setup(c => c.CachedCallAsync(
"ERP", "GetOrder",
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
InstanceName,
It.IsAny<CancellationToken>(),
It.IsAny<TrackedOperationId?>(),
It.IsAny<Guid?>(), It.IsAny<string?>()))
.ReturnsAsync(new ExternalCallResult(true, null, null, WasBuffered: true));
var forwarder = new CapturingForwarder();
var helper = CreateHelper(client.Object, forwarder);
await helper.CachedCall("ERP", "GetOrder");
// The known TestExecutionId and SourceScript must reach the client
// unchanged — these are what the S&F retry loop persists and replays.
client.Verify(c => c.CachedCallAsync(
"ERP", "GetOrder",
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
InstanceName,
It.IsAny<CancellationToken>(),
It.IsAny<TrackedOperationId?>(),
It.Is<Guid?>(id => id == TestExecutionId),
It.Is<string?>(s => s == SourceScript)),
Times.Once);
}
[Fact]
public async Task CachedCall_ForwarderThrows_StillReturnsTrackedOperationId_OriginalCallProceeds()
{