feat(sitecall-audit): carry + persist SourceNode end-to-end via cached telemetry
Site: site emitters of SiteCallOperational (ExternalSystemClient, the script-API cached call path in ScriptRuntimeContext, CachedCallLifecycleBridge) inject INodeIdentityProvider and stamp SourceNode = NodeName at construction. OperationTrackingStore call site in CachedCallTelemetryForwarder now stamps SourceNode too. Central: SiteCallAuditRepository.UpsertAsync INSERT includes SourceNode in the column list; conditional monotonic UPDATE uses COALESCE(@SourceNode, SourceNode) so later packets cannot blank a previously- stamped value. After this commit every SiteCalls row carries node-a/node-b in SourceNode (subject to monotonic preservation).
This commit is contained in:
@@ -49,7 +49,8 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
string? lastError = null,
|
||||
DateTime? createdAtUtc = null,
|
||||
DateTime? updatedAtUtc = null,
|
||||
bool terminal = false)
|
||||
bool terminal = false,
|
||||
string? sourceNode = null)
|
||||
{
|
||||
var created = createdAtUtc ?? DateTime.UtcNow;
|
||||
var updated = updatedAtUtc ?? created;
|
||||
@@ -59,6 +60,7 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
Channel = "ApiOutbound",
|
||||
Target = "ERP.GetOrder",
|
||||
SourceSite = sourceSite,
|
||||
SourceNode = sourceNode,
|
||||
Status = status,
|
||||
RetryCount = retryCount,
|
||||
LastError = lastError,
|
||||
@@ -492,6 +494,67 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
Assert.Equal("corr-fault", response.CorrelationId);
|
||||
}
|
||||
|
||||
// ── SourceNode-stamping (Task 14): end-to-end actor → repo persistence ──
|
||||
|
||||
[SkippableFact]
|
||||
public async Task UpsertSiteCallCommand_PersistsSourceNode_EndToEnd()
|
||||
{
|
||||
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||
|
||||
// SourceNode-stamping (Task 14): an UpsertSiteCallCommand carrying
|
||||
// SourceNode "node-a" must land in the SiteCalls row's SourceNode
|
||||
// column unchanged — verifies the actor's mapping path does not
|
||||
// strip the column AND the repository INSERT writes it.
|
||||
var siteId = NewSiteId();
|
||||
var id = TrackedOperationId.New();
|
||||
var row = NewRow(id, siteId, status: "Submitted", sourceNode: "node-a");
|
||||
|
||||
await using var context = CreateContext();
|
||||
var repo = new SiteCallAuditRepository(context);
|
||||
var actor = CreateActor(repo);
|
||||
|
||||
actor.Tell(new UpsertSiteCallCommand(row), TestActor);
|
||||
var reply = ExpectMsg<UpsertSiteCallReply>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(reply.Accepted);
|
||||
|
||||
await using var readContext = CreateContext();
|
||||
var stored = await readContext.Set<SiteCall>()
|
||||
.Where(s => s.TrackedOperationId == id)
|
||||
.ToListAsync();
|
||||
Assert.Single(stored);
|
||||
Assert.Equal("node-a", stored[0].SourceNode);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task UpsertSiteCallCommand_NullSourceNode_PersistsNull()
|
||||
{
|
||||
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||
|
||||
// Mirror of the above for unstamped packets — a command with null
|
||||
// SourceNode persists NULL on the row rather than falling back to a
|
||||
// placeholder. The first audit packet from a legacy host (or a node
|
||||
// without INodeIdentityProvider wired) must NOT inject a fabricated
|
||||
// value central-side.
|
||||
var siteId = NewSiteId();
|
||||
var id = TrackedOperationId.New();
|
||||
var row = NewRow(id, siteId, status: "Submitted", sourceNode: null);
|
||||
|
||||
await using var context = CreateContext();
|
||||
var repo = new SiteCallAuditRepository(context);
|
||||
var actor = CreateActor(repo);
|
||||
|
||||
actor.Tell(new UpsertSiteCallCommand(row), TestActor);
|
||||
var reply = ExpectMsg<UpsertSiteCallReply>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(reply.Accepted);
|
||||
|
||||
await using var readContext = CreateContext();
|
||||
var stored = await readContext.Set<SiteCall>()
|
||||
.Where(s => s.TrackedOperationId == id)
|
||||
.ToListAsync();
|
||||
Assert.Single(stored);
|
||||
Assert.Null(stored[0].SourceNode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test double whose <see cref="ISiteCallAuditRepository.QueryAsync"/> always
|
||||
/// throws — used to verify the query handler's failure projection produces a
|
||||
|
||||
Reference in New Issue
Block a user