17 KiB
WaitAsync Deferred Optional Items — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans (subagent-driven) to implement this plan task-by-task.
Goal: Implement the three items deferred from the WaitAsync spec (docs/plans/2026-06-17-waitfor-attribute-change-helper-spec.md): §3 WaitForAsync/WaitResult richer overload, §4.2 quality-gated ("Good"-only) matching, and §6 inbound/routed Route.To(...).WaitForAttribute variant.
Architecture: Builds on the shipped core (b89d69a→04e97f4). Two of the items (§3, §4.2) are site-local enrichments of the existing Attributes script surface + InstanceActor waiter; no new actor protocol shapes beyond an additive RequireGoodQuality field. The third (§6) mirrors the existing Route.To(...).GetAttributes cross-cluster path end-to-end (RouteTarget → IInstanceRouter → CommunicationService → SiteCommunicationActor → DeploymentManagerActor → InstanceActor), value-equality only across the wire, with the cluster Ask bounded by the wait timeout rather than the generic integration timeout.
Tech Stack: C#/.NET 10, Akka.NET 1.5, xUnit + Akka.TestKit + NSubstitute.
Branch/worktree: waitfor-attr-helper at /Users/dohertj2/Desktop/ScadaBridge/.claude/worktrees/waitfor-attr-helper (off local main; carries the core feature). Implementers do NOT create worktrees, commit pathspec form (git commit -m "…" -- <paths>), do NOT push, do NOT touch main. Targeted builds/tests per task; full-solution build only in WD-3.
Naming / shared shapes
- New script return type
WaitResult(Commons):public readonly record struct WaitResult(bool Matched, object? Value, string? Quality, bool TimedOut); WaitForAttributeRequestgains a trailing additive fieldbool RequireGoodQuality = false(site-local request).RequireGoodQualitysemantics: a match requires the value test to pass andstring.Equals(quality, "Good", StringComparison.Ordinal).- Routed contract (value-equality only, no predicate, no quality flag across the wire — §6 says value-equality only):
RouteToWaitForAttributeRequest/RouteToWaitForAttributeResponse(CommonsMessages/InboundApi). - The
WaitForAttributeResponse.Qualityfield is alreadystring?(null on timeout/error).
Execution waves
- Wave 1 (parallel, disjoint files): WD-1 ∥ WD-2a. (2 concurrent committers; post-wave HEAD-presence check.)
- Wave 2: WD-2b (after WD-2a).
- Wave 3: WD-3 (after WD-1, WD-2a, WD-2b).
WD-1 must add RequireGoodQuality ONLY as a trailing defaulted ctor param of WaitForAttributeRequest, so WD-2b's new WaitForAttributeRequest(...) (built in wave 2) compiles regardless.
Task WD-1: Site-local WaitForAsync + WaitResult + quality-gated mode (§3 + §4.2)
Classification: high-risk (modifies the InstanceActor single-threaded match evaluation + an additive message-contract field)
Estimated implement time: ~5 min
Parallelizable with: WD-2a
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.Commons/Types/WaitResult.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/WaitForAttribute.cs(add trailingbool RequireGoodQuality = falsetoWaitForAttributeRequest) - Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs(threadRequireGoodQualityintoPendingWait+ both match sites) - Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs(addWaitAttributeFullreturningWaitResult; addrequireGoodQualityparam) - Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs(addWaitForAsyncoverloads +requireGoodQualityoptional param onWaitAsync) - Test:
tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/InstanceActorWaitForAttributeTests.cs+tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Scripts/ScopeAccessorTests.cs
Steps (TDD):
-
WaitResult— add the readonly record struct above. -
WaitForAttributeRequest— add trailingbool RequireGoodQuality = false. Keep theFunc<>predicate field as-is. Update the XML-doc. -
InstanceActor— addbool RequireGoodQualityto thePendingWaitrecord. At BOTH match sites build the effective match as:// fast-path (HandleWaitForAttribute): quality from _attributeQualities.GetValueOrDefault(name, <existing default>) // resolve loop (ResolveMatchedWaiters): quality from changed.Quality bool QualityOk(string? q) => !requireGoodQuality || string.Equals(q, "Good", StringComparison.Ordinal); bool matched = QualityOk(quality) && test(value); // keep test() inside its existing try/catchStore
RequireGoodQualityon thePendingWaitso the resolve loop knows it. Keep the throwing-predicate guard (theQualityOk && testmust still be inside the existing try/catch). The fast-path quality-fail whenrequireGoodQualityis just a non-match → register + schedule timeout as normal (do NOT fast-reply matched). -
ScriptRuntimeContext— refactor: a privateTask<WaitForAttributeResponse> WaitInternal(name, encoded, predicate, timeout, requireGoodQuality)that does the token-boundedAsk(keep the existingAskTimeoutException → ...handling; on AskTimeout return a syntheticWaitForAttributeResponse(.., Matched:false, TimedOut:true)). Then:public async Task<bool> WaitAttribute(string name, string? enc, Func<object?,bool>? pred, TimeSpan t, bool requireGoodQuality = false) => (await WaitInternal(name, enc, pred, t, requireGoodQuality)).Matched; public async Task<WaitResult> WaitAttributeFull(string name, string? enc, Func<object?,bool>? pred, TimeSpan t, bool requireGoodQuality = false) { var r = await WaitInternal(...); return new WaitResult(r.Matched, r.Value, r.Quality, r.TimedOut); }(Note:
WaitAttribute's existingAskTimeoutException → return falsemust be preserved — fold it intoWaitInternalreturning a non-matched/timed-out response, OR catch in both. Do NOT catchOperationCanceledException/TaskCanceledException.) -
AttributeAccessor— addrequireGoodQualityoptional param to both existingWaitAsyncoverloads, and add twoWaitForAsyncoverloads:public Task<WaitResult> WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false) => _ctx.WaitAttributeFull(Resolve(key), AttributeValueCodec.Encode(targetValue), null, timeout, requireGoodQuality); public Task<WaitResult> WaitForAsync(string key, Func<object?,bool> predicate, TimeSpan timeout, bool requireGoodQuality = false) => _ctx.WaitAttributeFull(Resolve(key), null, predicate, timeout, requireGoodQuality);XML-doc:
requireGoodQuality:trueignores Bad/Uncertain-quality transients. -
Tests (extend existing files): (a)
WaitForAsyncreturns a populatedWaitResulton match (Value+Quality) and on timeout (Matched:false, TimedOut:true). (b) quality-gated: a value reaching target at Bad quality does NOT match whenrequireGoodQuality:true(stays pending → times out), but DOES match whenfalse; and matches when it reaches target at Good quality. Cover both fast-path (already-at-target-but-Bad) and change-match. (c) scope resolution still applied forWaitForAsync. -
Build
Commons+SiteRuntime+ the SiteRuntime test project; run--filter "FullyQualifiedName~WaitForAttribute|FullyQualifiedName~WaitAsync|FullyQualifiedName~WaitForAsync"and the~InstanceActor|~ScopeAccessorregression filter. All green. -
Commit (pathspec).
Task WD-2a: Routed contract + central path (§6, part 1)
Classification: high-risk (cross-cluster message contract + IInstanceRouter surface)
Estimated implement time: ~5 min
Parallelizable with: WD-1
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Messages/InboundApi/RouteToInstanceRequest.cs(add the two records) - Modify:
src/ZB.MOM.WW.ScadaBridge.InboundAPI/IInstanceRouter.cs(add method) - Modify:
src/ZB.MOM.WW.ScadaBridge.InboundAPI/CommunicationServiceInstanceRouter.cs(delegate) - Modify:
src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs(RouteTarget.WaitForAttribute) - Modify:
src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs(RouteToWaitForAttributeAsync— wait-timeout-aware Ask) - Modify (compile-break fixes — interface gained a member):
tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ParentExecutionIdCorrelationTests.cs(BridgingInstanceRouter) and the inlineIInstanceRouterdouble intests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/EndpointContentTypeTests.cs - Test:
tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/RouteHelperTests.cs
Steps (TDD):
-
Commons records (mirror
RouteToGetAttributes*, value-equality only):public record RouteToWaitForAttributeRequest( string CorrelationId, string InstanceUniqueName, string AttributeName, string? TargetValueEncoded, TimeSpan Timeout, DateTimeOffset Timestamp, Guid? ParentExecutionId = null); public record RouteToWaitForAttributeResponse( string CorrelationId, bool Matched, object? Value, string? Quality, bool TimedOut, bool Success, string? ErrorMessage, DateTimeOffset Timestamp);(
Success/ErrorMessage= routing-level outcome, e.g. instance-not-found;Matched/TimedOut/Value/Quality= wait outcome.) -
IInstanceRouter— addTask<RouteToWaitForAttributeResponse> RouteToWaitForAttributeAsync(string siteId, RouteToWaitForAttributeRequest request, CancellationToken cancellationToken);. Update all 3 implementers (prodCommunicationServiceInstanceRouter+ the 2 test doubles listed above; the test doubles can return a canned response / throw NotImplemented only if never exercised — prefer a sane canned response). -
CommunicationServiceInstanceRouter— delegate to_communicationService.RouteToWaitForAttributeAsync(...). -
RouteHelper.RouteTarget— add (mirrorGetAttributes, throw on!Success):public async Task<bool> WaitForAttribute(string attributeName, object? targetValue, TimeSpan timeout, CancellationToken cancellationToken = default) { var token = Effective(cancellationToken); var siteId = await ResolveSiteAsync(token); var request = new RouteToWaitForAttributeRequest(Guid.NewGuid().ToString(), _instanceCode, attributeName, AttributeValueCodec.Encode(targetValue), timeout, DateTimeOffset.UtcNow, _parentExecutionId); var response = await _instanceRouter.RouteToWaitForAttributeAsync(siteId, request, token); if (!response.Success) throw new InvalidOperationException(response.ErrorMessage ?? "Remote attribute wait failed"); return response.Matched; }(
AttributeValueCodecis in Commons.Types — add the using if needed.) -
CommunicationService.RouteToWaitForAttributeAsync— mirrorRouteToGetAttributesAsyncBUT bound the Ask by the wait timeout, not the generic integration timeout:var envelope = new SiteEnvelope(siteId, request); var askTimeout = request.Timeout + _options.IntegrationTimeout; // slack beyond the wait return await GetActor().Ask<RouteToWaitForAttributeResponse>(envelope, askTimeout, cancellationToken); -
Test (
RouteHelperTests): with a substituteIInstanceRouterreturning a cannedRouteToWaitForAttributeResponse(Matched:true,...),Route.To("x").WaitForAttribute("Flag", true, 30s)returns true;Success:false→ throwsInvalidOperationException; the encoded target equalsAttributeValueCodec.Encode(true). -
Build
Commons+InboundAPI+Communication+ the two affected test projects; run--filter "FullyQualifiedName~RouteHelper"+ a build of AuditLog.Tests/InboundAPI.Tests to confirm the interface-addition compiles. Commit (pathspec).
Task WD-2b: Site unpacking + handler (§6, part 2)
Classification: high-risk (actor handler crossing into InstanceActor; Ask-timeout correctness)
Estimated implement time: ~4 min
Parallelizable with: none
blockedBy: WD-2a
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs(addReceive<RouteToWaitForAttributeRequest>(msg => _deploymentManagerProxy.Forward(msg));next to the other RouteTo forwards ~line 145) - Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs(Receive<RouteToWaitForAttributeRequest>(RouteInboundApiWaitForAttribute);+ handler) - Test:
tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/DeploymentManagerActorTests.cs
Steps (TDD):
-
SiteCommunicationActor— add theReceive/Forward line. -
DeploymentManagerActor.RouteInboundApiWaitForAttribute— mirrorRouteInboundApiGetAttributes:private void RouteInboundApiWaitForAttribute(RouteToWaitForAttributeRequest request) { if (!_instanceActors.TryGetValue(request.InstanceUniqueName, out var instanceActor)) { Sender.Tell(new RouteToWaitForAttributeResponse(request.CorrelationId, false, null, null, false, false, $"Instance '{request.InstanceUniqueName}' not found on this site.", DateTimeOffset.UtcNow)); return; } var sender = Sender; var inner = new WaitForAttributeRequest(request.CorrelationId, request.InstanceUniqueName, request.AttributeName, request.TargetValueEncoded, null /*predicate*/, request.Timeout, DateTimeOffset.UtcNow /*, RequireGoodQuality defaults false */); // Ask bounded by the WAIT timeout + slack (NOT a fixed 30s). instanceActor.Ask<WaitForAttributeResponse>(inner, request.Timeout + TimeSpan.FromSeconds(5)) .ContinueWith(t => t.IsCompletedSuccessfully ? new RouteToWaitForAttributeResponse(request.CorrelationId, t.Result.Matched, t.Result.Value, t.Result.Quality, t.Result.TimedOut, true, null, DateTimeOffset.UtcNow) : new RouteToWaitForAttributeResponse(request.CorrelationId, false, null, null, false, false, t.Exception?.GetBaseException().Message ?? "Attribute wait timed out", DateTimeOffset.UtcNow)) .PipeTo(sender); }(
WaitForAttributeRequestlives in CommonsMessages/Instance— add the using. Build with both the trailing-RequireGoodQualityand pre-field signatures in mind; passing 7 positional args + default is fine.) -
Test (
DeploymentManagerActorTests, mirror the routed get-attributes test): deploy/register an instance whose attribute already equals the target →RouteToWaitForAttributeRequest→RouteToWaitForAttributeResponse(Success:true, Matched:true); unknown instance →Success:false. -
Build
Communication+SiteRuntime+ SiteRuntime test project; run--filter "FullyQualifiedName~DeploymentManagerActor". Commit (pathspec).
Task WD-3: Integration — docs + full verification
Classification: standard Estimated implement time: ~4 min Parallelizable with: none blockedBy: WD-1, WD-2a, WD-2b
Files:
- Modify:
docs/plans/2026-06-17-waitfor-attribute-change-helper-spec.md(mark §3WaitForAsync/WaitResult, §4.2 quality-gated mode, and §6 routed variant as IMPLEMENTED; note Test-Run sandbox parity excluded) - Modify:
docs/requirements/Component-SiteRuntime.md(script-surface note:Attributes.WaitForAsync+requireGoodQuality) anddocs/requirements/Component-InboundAPI.md(Route.To(...).WaitForAttribute) — brief, only if those docs enumerate the script surface - (No new component, no migration, no docker config change)
Steps:
- Update the spec doc + component docs as above.
- Full-solution build:
dotnet build ZB.MOM.WW.ScadaBridge.slnx— 0 errors. - Targeted test sweep across everything touched:
dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/... --filter "FullyQualifiedName~WaitForAttribute|FullyQualifiedName~WaitAsync|FullyQualifiedName~WaitForAsync|FullyQualifiedName~DeploymentManagerActor",dotnet test tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/... --filter "FullyQualifiedName~RouteHelper", and a build oftests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests+tests/ZB.MOM.WW.ScadaBridge.Communication.Teststo confirm no compile/regression from the interface addition. git diffreview; commit (pathspec).
Out of scope (explicit)
- Routed
WaitForAttributeis NOT wired into the CentralUI Test-Run sandbox (ISandboxInstanceGateway/SandboxInstanceGateway); production inbound scripts get it. Follow-up if Test-Run parity is wanted. - No predicate or quality flag across the wire (§6 is value-equality only, per spec).
- No docker redeploy (no cluster-runtime config change; additive script surface only).