feat(reconcile): site-reconcile messages + expected-set/stage-if-absent repo

- Commons: ReconcileSiteRequest / ReconcileSiteResponse / ReconcileGapItem
  message contracts (site→central ClusterClient on startup; central reply with
  gap fetch tokens + orphan list + base URL).
- Commons: ExpectedDeployment projection record (Commons/Types/Deployment/),
  lightweight join of DeployedConfigSnapshot + Instance (no ConfigJson).
- IDeploymentManagerRepository: GetExpectedDeploymentsForSiteAsync (inner-join
  query returning deployed set for a site, excluding snapshot-less instances) +
  StagePendingIfAbsentAsync (insert-if-absent, self-contained save, returns bool;
  does NOT supersede — an existing pending row signals in-flight delivery).
- DeploymentManagerRepository: implement both methods; StagePendingIfAbsent
  commits internally (matches PurgeExpiredPendingDeployments convention).
- ReconcileRepositoryTests: 4 tests covering expected-set filter/IsEnabled/
  cross-site isolation, empty-site, stage-absent (true + row retrievable),
  stage-present (false + existing row unchanged); all pass.
This commit is contained in:
Joseph Doherty
2026-06-26 16:04:12 -04:00
parent e5503857df
commit ec2aa2bbac
6 changed files with 333 additions and 0 deletions
@@ -0,0 +1,11 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
/// <summary>
/// Site→central (over ClusterClient) on node startup: the node's local deployed inventory,
/// so central can reply with fetch tokens for whatever the node is missing or stale
/// (self-heal a node that was down during a deploy).
/// </summary>
public record ReconcileSiteRequest(
string SiteIdentifier,
string NodeId,
IReadOnlyDictionary<string, string> LocalNameToRevisionHash);
@@ -0,0 +1,21 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
/// <summary>
/// Central→site reply: fresh fetch tokens for the gap (instances the node is missing or has at
/// a stale revision), orphan names to log (present locally but no longer deployed centrally),
/// and the base URL to fetch from.
/// </summary>
public record ReconcileSiteResponse(
IReadOnlyList<ReconcileGapItem> Gap,
IReadOnlyList<string> OrphanNames,
string CentralFetchBaseUrl);
/// <summary>
/// One instance the node must (re)fetch, with a freshly-minted short-TTL token.
/// </summary>
public record ReconcileGapItem(
string InstanceUniqueName,
string DeploymentId,
string RevisionHash,
bool IsEnabled,
string FetchToken);