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
@@ -2,8 +2,38 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
/// <summary>
/// Snapshot of an instance's debug state returned in response to a
/// <see cref="DebugSnapshotRequest"/> or <see cref="SubscribeDebugViewRequest"/>.
/// </summary>
/// <remarks>
/// <para>
/// <b>Additive-only contract (M2.11):</b> <see cref="InstanceNotFound"/> is an
/// optional trailing parameter with a default of <see langword="false"/> so every
/// existing positional constructor call and every existing serialized wire frame
/// remains valid. Callers that receive a snapshot with
/// <c>InstanceNotFound = true</c> know the instance was unknown on the site and
/// should distinguish that from a deployed-but-empty instance
/// (<c>InstanceNotFound = false</c>, empty <see cref="AttributeValues"/> and
/// <see cref="AlarmStates"/>).
/// </para>
/// <para>
/// A new dedicated message type (<c>DebugViewInstanceNotFound</c>) was
/// considered but rejected: the ClusterClient / ClusterClientReceptionist
/// channel is typed on the request side and the bridge actor is already
/// pattern-matching on <c>DebugViewSnapshot</c> for the initial-snapshot TCS
/// in <c>DebugStreamService</c>. Introducing a second reply type would require
/// every consumer to handle an additional <c>Ask</c> result union — more change
/// for no additive-safety gain. The defaulted field is strictly additive and
/// keeps all call sites untouched.
/// </para>
/// </remarks>
public record DebugViewSnapshot(
string InstanceUniqueName,
IReadOnlyList<AttributeValueChanged> AttributeValues,
IReadOnlyList<AlarmStateChanged> AlarmStates,
DateTimeOffset SnapshotTimestamp);
DateTimeOffset SnapshotTimestamp,
// M2.11 — additive field: true when the requested instance is not registered
// on this site. Defaults to false so all existing call sites and wire
// frames are unaffected.
bool InstanceNotFound = false);