feat(testrun): route WaitForAttribute in the Test-Run sandbox (deferred #14)

Closes deferred-work register item #14 with full fidelity. The Central UI Test-Run
sandbox previously threw ScriptSandboxException for Attributes.WaitAsync/WaitForAsync;
now the value-equality forms route to the bound deployed instance over the same
cross-site path inbound Route.To().WaitForAttribute() uses.

- Commons: additive trailing RouteToWaitForAttributeRequest.RequireGoodQuality
  (default false; message-evolution safe) so quality-gated waits route too.
- SiteRuntime: DeploymentManagerActor's routed-wait handler now threads
  request.RequireGoodQuality into the InstanceActor WaitForAttributeRequest
  (previously hard-coded to default).
- CentralUI sandbox: ISandboxInstanceGateway.WaitForAttributeAsync +
  SandboxInstanceGateway impl (via CommunicationService.RouteToWaitForAttributeAsync);
  SandboxAttributeAccessor value-equality WaitAsync/WaitForAsync route through it
  (codec-encoded target, scope-resolved name). Predicate overloads stay unsupported
  (an in-process lambda can't be routed) and throw a clearer, dedicated message.
- Tests: sandbox accessor routing + predicate/unbound rejection (CentralUI);
  site-handler RequireGoodQuality threading (SiteRuntime). Register + the historical
  waitfor-deferred plan doc updated.

Full slnx build 0/0; CentralUI 59, SiteRuntime routed-wait 4, InboundAPI wait 32 green.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 09:29:38 -04:00
parent 4cd21b342b
commit 22136d36d9
8 changed files with 216 additions and 31 deletions
@@ -551,6 +551,35 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
Assert.Equal("Good", response.Quality);
}
[Fact]
public async Task RouteInboundApiWaitForAttribute_RequireGoodQuality_IsThreadedToInstanceActor()
{
// #14 (full fidelity): the additive RouteToWaitForAttributeRequest.RequireGoodQuality
// flag must reach the InstanceActor's wait. A static attribute is Good quality, so a
// quality-gated wait whose target equals the current value still matches — proving the
// flag threads through (and does not spuriously reject a Good value).
var actor = CreateDeploymentManager();
await Task.Delay(500);
actor.Tell(new DeployInstanceCommand(
"dep-wait-gq", "WaitPumpGq", "sha256:wait-gq",
MakeConfigJson("WaitPumpGq"), "admin", DateTimeOffset.UtcNow));
ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
await Task.Delay(1000);
var encodedTarget = AttributeValueCodec.Encode("42");
actor.Tell(new RouteToWaitForAttributeRequest(
"wait-corr-gq", "WaitPumpGq", "TestAttr", encodedTarget,
TimeSpan.FromSeconds(5), DateTimeOffset.UtcNow,
ParentExecutionId: null, RequireGoodQuality: true));
var response = ExpectMsg<RouteToWaitForAttributeResponse>(TimeSpan.FromSeconds(10));
Assert.Equal("wait-corr-gq", response.CorrelationId);
Assert.True(response.Success, $"Routed quality-gated wait failed: {response.ErrorMessage}");
Assert.True(response.Matched, "A Good-quality attribute at target must satisfy a RequireGoodQuality wait.");
Assert.Equal("Good", response.Quality);
}
[Fact]
public async Task RouteInboundApiWaitForAttribute_UnknownInstance_RepliesNotFound()
{