feat(auditlog): site script-side emitters stamp ExecutionId

Move the per-script-execution Guid on ScriptRuntimeContext from
_auditCorrelationId to _executionId, and stamp it into the dedicated
AuditEvent.ExecutionId column on every script-side audit row:

- Sync ApiCall / DbWrite: ExecutionId set; CorrelationId reverts to
  null (a sync one-shot call has no operation lifecycle).
- Cached-call script-side rows (CachedSubmit, immediate-completion
  ApiCallCached + CachedResolve) and NotifySend: ExecutionId set;
  CorrelationId unchanged (per-operation TrackedOperationId /
  NotificationId).

Renames the threaded ctor param/field across ExternalSystemHelper,
DatabaseHelper, AuditingDbConnection and AuditingDbCommand, and threads
the id through NotifyHelper/NotifyTarget. The S&F retry-loop cached rows
(CachedCallLifecycleBridge) are out of scope here.
This commit is contained in:
Joseph Doherty
2026-05-21 15:05:00 -04:00
parent 6b16a48886
commit 0149ce6180
10 changed files with 193 additions and 95 deletions

View File

@@ -49,27 +49,27 @@ public class DatabaseSyncEmissionTests
private const string ConnectionName = "machineData";
/// <summary>
/// Audit Log #23: a fixed execution-wide correlation id used by the
/// default <see cref="CreateHelper(IDatabaseGateway, IAuditWriter?)"/>
/// Audit Log #23: a fixed per-execution id used by the default
/// <see cref="CreateHelper(IDatabaseGateway, IAuditWriter?)"/>
/// overload so assertions can compare against a known value.
/// </summary>
private static readonly Guid TestCorrelationId = Guid.NewGuid();
private static readonly Guid TestExecutionId = Guid.NewGuid();
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
IDatabaseGateway gateway,
IAuditWriter? auditWriter)
=> CreateHelper(gateway, auditWriter, TestCorrelationId);
=> CreateHelper(gateway, auditWriter, TestExecutionId);
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
IDatabaseGateway gateway,
IAuditWriter? auditWriter,
Guid correlationId)
Guid executionId)
{
return new ScriptRuntimeContext.DatabaseHelper(
gateway,
InstanceName,
NullLogger.Instance,
correlationId,
executionId,
auditWriter: auditWriter,
siteId: SiteId,
sourceScript: SourceScript,
@@ -282,14 +282,16 @@ public class DatabaseSyncEmissionTests
Assert.Equal(SourceScript, evt.SourceScript);
// Outbound channel: Actor carries the calling script identity.
Assert.Equal(SourceScript, evt.Actor);
// Audit Log #23: the sync DbWrite row now carries the execution-wide
// correlation id the helper was constructed with.
Assert.Equal(TestCorrelationId, evt.CorrelationId);
// Audit Log #23: the sync DbWrite row carries the per-execution id the
// helper was constructed with in ExecutionId. CorrelationId is null —
// a sync one-shot call has no operation lifecycle.
Assert.Equal(TestExecutionId, evt.ExecutionId);
Assert.Null(evt.CorrelationId);
Assert.NotEqual(Guid.Empty, evt.EventId);
}
[Fact]
public async Task SyncDbWrite_StampsExecutionCorrelationId()
public async Task SyncDbWrite_StampsExecutionId_AndNullCorrelationId()
{
using var keepAlive = new SqliteConnection("Data Source=kc;Mode=Memory;Cache=Shared");
var inner = NewInMemoryDb(out var _);
@@ -298,16 +300,18 @@ public class DatabaseSyncEmissionTests
.Setup(g => g.GetConnectionAsync(ConnectionName, It.IsAny<CancellationToken>()))
.ReturnsAsync(inner);
var writer = new CapturingAuditWriter();
var correlationId = Guid.NewGuid();
var executionId = Guid.NewGuid();
var helper = CreateHelper(gateway.Object, writer, correlationId);
var helper = CreateHelper(gateway.Object, writer, executionId);
await using var conn = await helper.Connection(ConnectionName);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO t (id, name) VALUES (7, 'eta')";
await cmd.ExecuteNonQueryAsync();
var evt = Assert.Single(writer.Events);
Assert.Equal(correlationId, evt.CorrelationId);
Assert.Equal(executionId, evt.ExecutionId);
// Sync one-shot call: CorrelationId is null (no operation lifecycle).
Assert.Null(evt.CorrelationId);
}
[Fact]