fix(siteruntime): M2.11 — unknown-instance debug snapshot returns InstanceNotFound=true (#24)

RouteDebugSnapshot and RouteDebugViewSubscribe on DeploymentManagerActor
previously returned an empty DebugViewSnapshot for unknown instances,
indistinguishable from a deployed-but-empty instance. Callers had no way
to differentiate "not deployed here" from "deployed, no data yet."

Approach — additive field on existing message contract:
  Added `bool InstanceNotFound = false` as an optional trailing parameter
  to DebugViewSnapshot (Commons). All existing positional constructor calls
  and serialized wire frames are unaffected (default = false). A dedicated
  new message type was considered but rejected: the ClusterClient channel
  and DebugStreamService TCS are already typed on DebugViewSnapshot, and a
  second reply union would require wider changes for zero additive-safety
  gain.

Changes:
  - Commons/DebugViewSnapshot: add InstanceNotFound = false (additive)
  - DeploymentManagerActor: set InstanceNotFound=true in both unknown-
    instance branches (RouteDebugViewSubscribe, RouteDebugSnapshot)
  - DebugStreamBridgeActor: when snapshot.InstanceNotFound, forward it to
    _onEvent (resolves the TCS) then stop cleanly; no gRPC stream opened
  - DebugView.razor: check session.InitialSnapshot.InstanceNotFound after
    connect and show a clear "not deployed on this site" error toast
  - 3 new tests in DeploymentManagerActorTests covering: unknown→snapshot,
    unknown→subscribe, known-empty→InstanceNotFound stays false
This commit is contained in:
Joseph Doherty
2026-06-16 06:08:21 -04:00
parent 9cd62aa5b4
commit dbf44b9e10
5 changed files with 127 additions and 4 deletions
@@ -3,6 +3,7 @@ using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Lifecycle;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
@@ -387,4 +388,64 @@ public class DeploymentManagerActorTests : TestKit, IDisposable
Assert.Equal("route-corr-2", response.CorrelationId);
Assert.True(response.Success, $"Routed call failed: {response.ErrorMessage}");
}
// ── M2.11: Debug-view routing — unknown-instance not-found signal ──
[Fact]
public async Task RouteDebugSnapshot_UnknownInstance_SetsInstanceNotFound()
{
// An instance that was never deployed to this site must return a
// DebugViewSnapshot with InstanceNotFound=true so the caller can
// distinguish "not deployed here" from a deployed-but-empty instance.
var actor = CreateDeploymentManager();
await Task.Delay(500);
actor.Tell(new DebugSnapshotRequest("NeverDeployed", "snap-corr-1"));
var snapshot = ExpectMsg<DebugViewSnapshot>(TimeSpan.FromSeconds(5));
Assert.Equal("NeverDeployed", snapshot.InstanceUniqueName);
Assert.True(snapshot.InstanceNotFound,
"Expected InstanceNotFound=true for an instance not registered on this site.");
Assert.Empty(snapshot.AttributeValues);
Assert.Empty(snapshot.AlarmStates);
}
[Fact]
public async Task RouteDebugViewSubscribe_UnknownInstance_SetsInstanceNotFound()
{
// Same contract for the subscribe path: unknown instance → InstanceNotFound=true.
var actor = CreateDeploymentManager();
await Task.Delay(500);
actor.Tell(new SubscribeDebugViewRequest("NeverDeployed2", "sub-corr-1"));
var snapshot = ExpectMsg<DebugViewSnapshot>(TimeSpan.FromSeconds(5));
Assert.Equal("NeverDeployed2", snapshot.InstanceUniqueName);
Assert.True(snapshot.InstanceNotFound,
"Expected InstanceNotFound=true for an instance not registered on this site.");
Assert.Empty(snapshot.AttributeValues);
Assert.Empty(snapshot.AlarmStates);
}
[Fact]
public async Task RouteDebugSnapshot_KnownButEmptyInstance_DoesNotSetInstanceNotFound()
{
// A deployed instance with no runtime data yet must return an empty
// snapshot with InstanceNotFound=false — the known-empty path is unchanged.
var actor = CreateDeploymentManager();
await Task.Delay(500);
actor.Tell(new DeployInstanceCommand(
"dep-dbg", "EmptyPump", "sha256:dbg",
MakeConfigJson("EmptyPump"), "admin", DateTimeOffset.UtcNow));
ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
await Task.Delay(800); // let InstanceActor spin up
actor.Tell(new DebugSnapshotRequest("EmptyPump", "snap-corr-2"));
var snapshot = ExpectMsg<DebugViewSnapshot>(TimeSpan.FromSeconds(5));
Assert.Equal("EmptyPump", snapshot.InstanceUniqueName);
Assert.False(snapshot.InstanceNotFound,
"A deployed (but empty) instance must NOT set InstanceNotFound.");
}
}