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

This commit is contained in:
Joseph Doherty
2026-05-21 17:45:55 -04:00
parent 6af2607a50
commit 150ba5e63f
9 changed files with 343 additions and 60 deletions

View File

@@ -63,7 +63,8 @@ public class DatabaseSyncEmissionTests
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
IDatabaseGateway gateway,
IAuditWriter? auditWriter,
Guid executionId)
Guid executionId,
Guid? parentExecutionId = null)
{
return new ScriptRuntimeContext.DatabaseHelper(
gateway,
@@ -73,7 +74,8 @@ public class DatabaseSyncEmissionTests
auditWriter: auditWriter,
siteId: SiteId,
sourceScript: SourceScript,
cachedForwarder: null);
cachedForwarder: null,
parentExecutionId: parentExecutionId);
}
/// <summary>
@@ -287,9 +289,62 @@ public class DatabaseSyncEmissionTests
// a sync one-shot call has no operation lifecycle.
Assert.Equal(TestExecutionId, evt.ExecutionId);
Assert.Null(evt.CorrelationId);
// Audit Log #23 (ParentExecutionId): null for a non-routed run — the
// default CreateHelper supplies no parentExecutionId.
Assert.Null(evt.ParentExecutionId);
Assert.NotEqual(Guid.Empty, evt.EventId);
}
[Fact]
public async Task SyncDbWrite_RoutedRun_StampsParentExecutionId_FromContext()
{
// Audit Log #23 (ParentExecutionId, Task 5): an inbound-API-routed run
// carries the spawning execution's id; the sync DbWrite row must stamp
// it in ParentExecutionId alongside its own fresh ExecutionId.
using var keepAlive = new SqliteConnection("Data Source=kp;Mode=Memory;Cache=Shared");
var inner = NewInMemoryDb(out var _);
var gateway = new Mock<IDatabaseGateway>();
gateway
.Setup(g => g.GetConnectionAsync(ConnectionName, It.IsAny<CancellationToken>()))
.ReturnsAsync(inner);
var writer = new CapturingAuditWriter();
var executionId = Guid.NewGuid();
var parentExecutionId = Guid.NewGuid();
var helper = CreateHelper(gateway.Object, writer, executionId, parentExecutionId);
await using var conn = await helper.Connection(ConnectionName);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO t (id, name) VALUES (9, 'theta')";
await cmd.ExecuteNonQueryAsync();
var evt = Assert.Single(writer.Events);
Assert.Equal(parentExecutionId, evt.ParentExecutionId);
Assert.Equal(executionId, evt.ExecutionId);
}
[Fact]
public async Task SyncDbWrite_NonRoutedRun_ParentExecutionIdIsNull()
{
// A normal (tag/timer) run is not routed — no parent id supplied, so
// the emitted DbWrite row's ParentExecutionId stays null.
using var keepAlive = new SqliteConnection("Data Source=kn;Mode=Memory;Cache=Shared");
var inner = NewInMemoryDb(out var _);
var gateway = new Mock<IDatabaseGateway>();
gateway
.Setup(g => g.GetConnectionAsync(ConnectionName, It.IsAny<CancellationToken>()))
.ReturnsAsync(inner);
var writer = new CapturingAuditWriter();
var helper = CreateHelper(gateway.Object, writer);
await using var conn = await helper.Connection(ConnectionName);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO t (id, name) VALUES (10, 'iota')";
await cmd.ExecuteNonQueryAsync();
var evt = Assert.Single(writer.Events);
Assert.Null(evt.ParentExecutionId);
}
[Fact]
public async Task SyncDbWrite_StampsExecutionId_AndNullCorrelationId()
{