refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.AuditLog` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.AuditLog` |
|
||||
| Design doc | `docs/requirements/Component-AuditLog.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -66,7 +66,7 @@ chain doesn't reject a central composition root that mistakenly calls the site b
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs:45`, `src/ScadaLink.AuditLog/Site/Telemetry/ClusterClientSiteAuditClient.cs:86`, `src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs:198` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ISiteStreamAuditClient.cs:45`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/ClusterClientSiteAuditClient.cs:86`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs:198` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -78,7 +78,7 @@ interface; `ClusterClientSiteAuditClient.IngestCachedTelemetryAsync` builds the
|
||||
`IngestCachedTelemetryCommand`; the proto carries `CachedTelemetryBatch`;
|
||||
`AuditLogIngestActor.OnCachedTelemetryAsync` performs the dual `InsertIfNotExists` +
|
||||
`UpsertAsync` inside a `BeginTransactionAsync`. But a `grep` for callers of
|
||||
`IngestCachedTelemetryAsync` in `src/ScadaLink.AuditLog` shows only the interface
|
||||
`IngestCachedTelemetryAsync` in `src/ZB.MOM.WW.ScadaBridge.AuditLog` shows only the interface
|
||||
declaration and the two implementations — nothing produces a `CachedTelemetryBatch` for
|
||||
the site to push. The `SiteAuditTelemetryActor.OnDrainAsync` only calls
|
||||
`IngestAuditEventsAsync` (the audit-only path); cached-call audit rows written by
|
||||
@@ -108,17 +108,17 @@ previously-unreachable `IngestCachedTelemetryAsync` client path now carries
|
||||
cached-call lifecycle rows from the site SQLite hot-path through to the central
|
||||
`AuditLogIngestActor.OnCachedTelemetryAsync` dual-write transaction. Changes:
|
||||
|
||||
- **`ISiteAuditQueue`** (`src/ScadaLink.Commons/Interfaces/Services/ISiteAuditQueue.cs`):
|
||||
- **`ISiteAuditQueue`** (`src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISiteAuditQueue.cs`):
|
||||
added `ReadPendingCachedTelemetryAsync(int, CancellationToken)` returning
|
||||
rows in `AuditForwardState.Pending` whose `Kind` is one of `CachedSubmit`,
|
||||
`ApiCallCached`, `DbWriteCached`, `CachedResolve`. Updated `ReadPendingAsync`
|
||||
XML doc to call out the partition.
|
||||
- **`SqliteAuditWriter`** (`src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs`):
|
||||
- **`SqliteAuditWriter`** (`src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs`):
|
||||
implemented `ReadPendingCachedTelemetryAsync` with a `Kind IN (...)` filter
|
||||
reusing the existing `_readConnection` / `_readLock` decoupling; modified
|
||||
`ReadPendingAsync` to add the symmetric `Kind NOT IN (...)` predicate so the
|
||||
audit-only drain no longer double-emits cached rows.
|
||||
- **`SiteAuditTelemetryActor`** (`src/ScadaLink.AuditLog/Site/Telemetry/SiteAuditTelemetryActor.cs`):
|
||||
- **`SiteAuditTelemetryActor`** (`src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SiteAuditTelemetryActor.cs`):
|
||||
added an optional `IOperationTrackingStore?` constructor parameter, a sibling
|
||||
`CachedDrain` self-tick message, and an `OnCachedDrainAsync` handler running
|
||||
in parallel with the existing audit-only drain. The cached-drain reads the
|
||||
@@ -130,12 +130,12 @@ cached-call lifecycle rows from the site SQLite hot-path through to the central
|
||||
blocks the rest of the batch; rows stay Pending and reconciliation /
|
||||
retention handles them. The lifecycle CTS (AuditLog-010) gates both drains
|
||||
uniformly.
|
||||
- **`AkkaHostedService`** (`src/ScadaLink.Host/Actors/AkkaHostedService.cs`):
|
||||
- **`AkkaHostedService`** (`src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs`):
|
||||
resolves `IOperationTrackingStore` via `GetService` (site-only registration)
|
||||
and threads it through the actor's `Props.Create`. Central composition
|
||||
roots and tests that don't register the tracking store get the legacy
|
||||
audit-only behaviour — the cached scheduler is never armed.
|
||||
- **Tests** (`tests/ScadaLink.AuditLog.Tests/Site/Telemetry/SiteAuditTelemetryActorTests.cs`):
|
||||
- **Tests** (`tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/Telemetry/SiteAuditTelemetryActorTests.cs`):
|
||||
added three regression tests asserting (1) cached rows route through
|
||||
`IngestCachedTelemetryAsync` and NOT `IngestAuditEventsAsync`, (2) an
|
||||
orphan row with no tracking snapshot is logged + skipped without crashing
|
||||
@@ -163,11 +163,11 @@ cached-call lifecycle rows from the site SQLite hot-path through to the central
|
||||
is shared so `PostStop` cancels in-flight cached lookups + pushes at the
|
||||
same instant as audit-only drains.
|
||||
|
||||
Build: `dotnet build ScadaLink.slnx` — 0 warnings, 0 errors.
|
||||
Tests: `dotnet test tests/ScadaLink.AuditLog.Tests` — 250 passed, 1 failed
|
||||
Build: `dotnet build ZB.MOM.WW.ScadaBridge.slnx` — 0 warnings, 0 errors.
|
||||
Tests: `dotnet test tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests` — 250 passed, 1 failed
|
||||
(`PartitionPurgeTests.EndToEnd_OldestPartition_PurgedViaActor_NewerKept` —
|
||||
pre-existing MS-SQL date-sensitive flake, called out in the prompt as
|
||||
acceptable). `dotnet test tests/ScadaLink.SiteRuntime.Tests` — all 302
|
||||
acceptable). `dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests` — all 302
|
||||
passed.
|
||||
|
||||
### AuditLog-002 — `SupervisorStrategy` comments claim Resume semantics but code returns the default Restart decider
|
||||
@@ -177,7 +177,7 @@ passed.
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs:99-103`, `src/ScadaLink.AuditLog/Central/AuditLogPurgeActor.cs:109-115`, `src/ScadaLink.AuditLog/Central/SiteAuditReconciliationActor.cs:315-321` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs:99-103`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs:109-115`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationActor.cs:315-321` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -224,7 +224,7 @@ override as a children-only forward-compat placeholder, and state the actual
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs:133`, `src/ScadaLink.AuditLog/Central/AuditLogPurgeActor.cs:139`, `src/ScadaLink.AuditLog/Central/SiteAuditReconciliationActor.cs:178` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs:133`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs:139`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationActor.cs:178` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -267,7 +267,7 @@ dispose asynchronously across every audit ingest path.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Central/SiteAuditReconciliationActor.cs:233-265` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationActor.cs:233-265` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -318,7 +318,7 @@ now tracks `_failedInsertAttempts: Dictionary<Guid, int>` and a per-tick
|
||||
The in-memory counter resets on singleton restart, which is safe because the
|
||||
cursor also resets and the next tick re-pulls everything. Tests for both the
|
||||
retry-hold and permanent-abandon paths should land alongside the existing
|
||||
reconciliation tests in `tests/ScadaLink.AuditLog.Tests/Central/` (deferred to
|
||||
reconciliation tests in `tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/` (deferred to
|
||||
the next coverage sweep — the logic is straightforward and the build/integration
|
||||
tests already exercise the success path).
|
||||
|
||||
@@ -329,7 +329,7 @@ tests already exercise the success path).
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs:597-657` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs:597-657` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -380,7 +380,7 @@ behind every batch INSERT under `_writeLock`).
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs:697-700` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs:697-700` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -426,7 +426,7 @@ documents the choice. Behaviour for context-free callers is unchanged.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs:148-218` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/ServiceCollectionExtensions.cs:148-218` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -477,10 +477,10 @@ resolution instead.
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Site/FallbackAuditWriter.cs:51-77`, `src/ScadaLink.AuditLog/Central/CentralAuditWriter.cs:77-104`, `src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs:125,155` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/FallbackAuditWriter.cs:51-77`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/CentralAuditWriter.cs:77-104`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs:125,155` |
|
||||
|
||||
**Resolution (2026-05-28):** New `SafeDefaultAuditPayloadFilter` in
|
||||
`src/ScadaLink.AuditLog/Payload/` — a stateless singleton that performs HTTP
|
||||
`src/ZB.MOM.WW.ScadaBridge.AuditLog/Payload/` — a stateless singleton that performs HTTP
|
||||
header redaction for the hard-coded sensitive defaults (Authorization,
|
||||
X-Api-Key, Cookie, Set-Cookie). The three writer-chain sites
|
||||
(`FallbackAuditWriter`, `CentralAuditWriter`, `AuditLogIngestActor` —
|
||||
@@ -528,7 +528,7 @@ _Unresolved._
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs:706-740` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs:706-740` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -572,7 +572,7 @@ the loop has drained (in the second lock block). Behaviour unchanged.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/Site/Telemetry/SiteAuditTelemetryActor.cs:92,107,124`, `src/ScadaLink.AuditLog/Central/SiteAuditReconciliationActor.cs:228` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SiteAuditTelemetryActor.cs:92,107,124`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationActor.cs:228` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -614,7 +614,7 @@ existing top-level catch swallows the `OperationCanceledException`.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.AuditLog/ServiceCollectionExtensions.cs:53-55, 263-276, 301-346` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.AuditLog/ServiceCollectionExtensions.cs:53-55, 263-276, 301-346` |
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.CLI` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.CLI` |
|
||||
| Design doc | `docs/requirements/Component-CLI.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -50,7 +50,7 @@ Critical/High issues; the module remains healthy.
|
||||
#### Re-review 2026-05-28 (commit `1eb6e97`)
|
||||
|
||||
The CLI has grown two substantial new command groups since the last re-review —
|
||||
`scadalink audit` (Audit Log #23 M8) and `scadalink bundle` (Transport #24) — together
|
||||
`scadabridge audit` (Audit Log #23 M8) and `scadabridge bundle` (Transport #24) — together
|
||||
adding ~1,500 lines of new production code. The new `audit` surface is well-tested and
|
||||
well-factored (pure helpers + a clear `IAuditFormatter` seam), but the new `bundle`
|
||||
surface is untested, duplicates the URL/credential resolution that already exists in
|
||||
@@ -70,7 +70,7 @@ findings (none Critical, three Medium).
|
||||
memory and writes synchronously — 100 MB bundles double-buffer.
|
||||
- **CLI-020** — `BundleCommands.bundle export` parses the success body with bare
|
||||
`JsonDocument.Parse` + `GetProperty` and throws on a malformed/abbreviated envelope.
|
||||
- **CLI-021** — `CliConfig.Load` crashes the whole CLI when `~/.scadalink/config.json`
|
||||
- **CLI-021** — `CliConfig.Load` crashes the whole CLI when `~/.scadabridge/config.json`
|
||||
is malformed or unreadable, even if `--url` was supplied on the command line.
|
||||
- **CLI-022** — `AuditCommands` and `BundleCommands` are absent from `CommandTreeTests`;
|
||||
the test still pins `Equal(14, groups.Count)` and silently excludes the new groups.
|
||||
@@ -133,7 +133,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:18`, `src/ScadaLink.CLI/Commands/DebugCommands.cs:45`, `src/ScadaLink.CLI/CliConfig.cs:37-39` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:18`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DebugCommands.cs:45`, `src/ZB.MOM.WW.ScadaBridge.CLI/CliConfig.cs:37-39` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -144,8 +144,8 @@ with `var format = result.GetValue(formatOption) ?? "json";` and `formatOption`
|
||||
in `Program.cs:11` with `DefaultValueFactory = _ => "json"`. `GetValue` therefore always
|
||||
returns a non-null value ("json" when the flag is absent), so the `?? "json"` fallback never
|
||||
fires and `config.DefaultFormat` is never consulted. The env var and config-file format
|
||||
settings are dead code: `scadalink site list` always outputs JSON regardless of
|
||||
`SCADALINK_FORMAT=table` or a `defaultFormat` entry in `~/.scadalink/config.json`. The
|
||||
settings are dead code: `scadabridge site list` always outputs JSON regardless of
|
||||
`SCADALINK_FORMAT=table` or a `defaultFormat` entry in `~/.scadabridge/config.json`. The
|
||||
documented behaviour silently does not work.
|
||||
|
||||
**Recommendation**
|
||||
@@ -171,7 +171,7 @@ now call `ResolveFormat`. Regression tests added in `FormatResolutionTests`.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:59-68`, `src/ScadaLink.CLI/Commands/CommandHelpers.cs:78-80` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:59-68`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:78-80` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -205,7 +205,7 @@ case, prints `(ok)`, and returns 0 before any parse. Regression tests added in
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:80` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:80` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -236,7 +236,7 @@ raw-body fallback on the JSON path. Regression test
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/ManagementHttpClient.cs:13` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/ManagementHttpClient.cs:13` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -268,7 +268,7 @@ http/https URL via `Uri.TryCreate`. Both `CommandHelpers.ExecuteCommandAsync` an
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/InstanceCommands.cs:55-58`, `src/ScadaLink.CLI/Commands/InstanceCommands.cs:181-182` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs:55-58`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs:181-182` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -305,7 +305,7 @@ wrong element types, and JSON null).
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Program.cs:9`, `src/ScadaLink.CLI/Commands/CommandHelpers.cs:36-44` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Program.cs:9`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:36-44` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -345,7 +345,7 @@ demands it. Regression tests in `CredentialResolutionTests`.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `docs/requirements/Component-CLI.md:51-211` (vs. all files under `src/ScadaLink.CLI/Commands/`) |
|
||||
| Location | `docs/requirements/Component-CLI.md:51-211` (vs. all files under `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -372,21 +372,21 @@ A reader following the design doc would be unable to drive the CLI.
|
||||
**Recommendation**
|
||||
|
||||
Regenerate the "Command Structure" section of `Component-CLI.md` from the actual command
|
||||
tree (the in-repo `src/ScadaLink.CLI/README.md` is much closer to reality and could be the
|
||||
tree (the in-repo `src/ZB.MOM.WW.ScadaBridge.CLI/README.md` is much closer to reality and could be the
|
||||
source), or mark the doc's command list as illustrative and point to the README as
|
||||
authoritative.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-16 (commit pending). Drift confirmed against every file under
|
||||
`src/ScadaLink.CLI/Commands/`. Regenerated the entire "Command Structure" section of
|
||||
`src/ZB.MOM.WW.ScadaBridge.CLI/Commands/`. Regenerated the entire "Command Structure" section of
|
||||
`Component-CLI.md` from the actual command tree: all entities are now keyed by integer
|
||||
`--id`; the non-existent `--file` option is removed; create/update commands list their
|
||||
real individual flags; non-existent commands (`template diff`, `instance
|
||||
bind-connections`/`assign-area`, `data-connection assign/unassign`, `security api-key
|
||||
enable/disable`) are removed; previously-omitted commands (`instance alarm-override
|
||||
set/delete/list`, `external-system method` subgroup, `site deploy-artifacts`) are added.
|
||||
A note now points to `src/ScadaLink.CLI/README.md` as the authoritative reference. The
|
||||
A note now points to `src/ZB.MOM.WW.ScadaBridge.CLI/README.md` as the authoritative reference. The
|
||||
Configuration section also documents the new `SCADALINK_USERNAME`/`SCADALINK_PASSWORD`
|
||||
env vars (see CLI-006).
|
||||
|
||||
@@ -397,7 +397,7 @@ env vars (see CLI-006).
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Program.cs:10-11`, `src/ScadaLink.CLI/Commands/CommandHelpers.cs:60` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Program.cs:10-11`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:60` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -426,7 +426,7 @@ rejected by `System.CommandLine` with a clear parse error. Regression tests in
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `docs/requirements/Component-CLI.md:238-249`, `src/ScadaLink.CLI/Commands/CommandHelpers.cs:75` |
|
||||
| Location | `docs/requirements/Component-CLI.md:238-249`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:75` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -469,7 +469,7 @@ surface, and the CLI's exit-code behaviour itself is now correct and pinned by t
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/DebugCommands.cs:181-189` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DebugCommands.cs:181-189` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -504,7 +504,7 @@ both paths. Regression tests in `DebugStreamTests` (`ClassifyConnectFailure_*`).
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/DebugCommands.cs:89` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DebugCommands.cs:89` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -535,7 +535,7 @@ covered indirectly by the `DebugStreamTests` exit-path tests.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/DebugCommands.cs:208-227` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DebugCommands.cs:208-227` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -575,7 +575,7 @@ solely through this helper. Regression tests in `DebugStreamTests`
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.CLI.Tests/` (vs. `src/ScadaLink.CLI/ManagementHttpClient.cs`, `src/ScadaLink.CLI/Commands/DebugCommands.cs`, `src/ScadaLink.CLI/Commands/InstanceCommands.cs:55-58`) |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/` (vs. `src/ZB.MOM.WW.ScadaBridge.CLI/ManagementHttpClient.cs`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DebugCommands.cs`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/InstanceCommands.cs:55-58`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -623,7 +623,7 @@ The CLI test suite went from 42 to 77 passing tests.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/TemplateCommands.cs:77`, `src/ScadaLink.CLI/Commands/SiteCommands.cs:86`, `src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs:40-42`, `src/ScadaLink.CLI/Commands/DataConnectionCommands.cs:39-40`, `src/ScadaLink.CLI/Commands/NotificationCommands.cs:40-41`, `src/ScadaLink.CLI/Commands/ApiMethodCommands.cs:79` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs:77`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/SiteCommands.cs:86`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/ExternalSystemCommands.cs:40-42`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DataConnectionCommands.cs:39-40`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/NotificationCommands.cs:40-41`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/ApiMethodCommands.cs:79` |
|
||||
|
||||
**Re-triage 2026-05-17:** the finding was originally filed as a Medium "Correctness &
|
||||
logic bugs" issue, but verification against the Commons message contracts shows the
|
||||
@@ -682,7 +682,7 @@ entity. Option (a) matches the documented surface and the typical CLI expectatio
|
||||
Resolved 2026-05-17 (commit pending) per recommendation option (b). Verification of the
|
||||
Commons `Update*Command` records confirmed whole-replace is the intentional contract, so
|
||||
the CLI's `Required = true` flags are correct and were left unchanged. The in-repo
|
||||
`src/ScadaLink.CLI/README.md` — which is authoritative and previously listed every
|
||||
`src/ZB.MOM.WW.ScadaBridge.CLI/README.md` — which is authoritative and previously listed every
|
||||
`update` core field as optional `[--name]` — was corrected: the core flags
|
||||
(`--name`/`--protocol`/`--script`/`--code`/`--emails`/`--endpoint-url`/`--auth-type`/
|
||||
`--data-type`/`--trigger-type`/`--priority`/`--connection-string`/`--ldap-group`/`--role`)
|
||||
@@ -701,10 +701,10 @@ surface — that doc-side correction is owned by the docs surface.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `docs/requirements/Component-CLI.md:75`, `docs/requirements/Component-CLI.md:125-126`, `src/ScadaLink.CLI/README.md` (vs. `src/ScadaLink.CLI/Commands/TemplateCommands.cs:404-413`, `src/ScadaLink.CLI/Commands/DataConnectionCommands.cs:41`, `:86`) |
|
||||
| Location | `docs/requirements/Component-CLI.md:75`, `docs/requirements/Component-CLI.md:125-126`, `src/ZB.MOM.WW.ScadaBridge.CLI/README.md` (vs. `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs:404-413`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DataConnectionCommands.cs:41`, `:86`) |
|
||||
|
||||
**Re-triage 2026-05-17:** verification found the same two drifts also present in the
|
||||
in-repo `src/ScadaLink.CLI/README.md` (the authoritative reference): its
|
||||
in-repo `src/ZB.MOM.WW.ScadaBridge.CLI/README.md` (the authoritative reference): its
|
||||
`template composition delete` section used the non-existent `--template-id` /
|
||||
`--instance-name` form, and `data-connection create`/`update` documented only
|
||||
`--configuration` without the canonical `--primary-config` flag (`--configuration` is in
|
||||
@@ -738,7 +738,7 @@ documented elsewhere.
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-17 (commit pending). Both drifts were present in the in-repo
|
||||
`src/ScadaLink.CLI/README.md` and were corrected there (the README is this module's
|
||||
`src/ZB.MOM.WW.ScadaBridge.CLI/README.md` and were corrected there (the README is this module's
|
||||
authoritative reference): `template composition delete` now documents the real single
|
||||
`--id <int>` form, and `data-connection create`/`update` now document `--primary-config`
|
||||
(with the `--configuration` alias noted) alongside `--backup-config` and
|
||||
@@ -754,7 +754,7 @@ outside this module's editable surface and remains for the docs surface to apply
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:184-200` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:184-200` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -794,7 +794,7 @@ first-element-extra column still rendered).
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/BundleCommands.cs:244-289` (vs. `src/ScadaLink.CLI/Commands/CommandHelpers.cs:20-73`, `:159-174`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs:244-289` (vs. `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs:20-73`, `:159-174`) |
|
||||
|
||||
**Resolution (2026-05-28):** Extended `CommandHelpers.ExecuteCommandAsync` with optional `timeout` and `onSuccess` parameters so a caller can supply a longer per-command timeout (`BundleCommandTimeout`) and capture the success body for file I/O. The duplicated `RunBundleCommandAsync` was deleted; all three `bundle` sub-commands now delegate through `ExecuteCommandAsync`, which routes the error path through `IsAuthorizationFailure` — exit 2 fires on HTTP 403 OR a `FORBIDDEN`/`UNAUTHORIZED` error code regardless of status, unifying the contract with every other command group.
|
||||
|
||||
@@ -835,7 +835,7 @@ messages verbatim.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/AuditQueryHelpers.cs:186-193`, `src/ScadaLink.CLI/Commands/AuditExportHelpers.cs:147-153` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditQueryHelpers.cs:186-193`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditExportHelpers.cs:147-153` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -889,7 +889,7 @@ and pass after.
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/BundleCommands.cs:117-124`, `src/ScadaLink.CLI/ManagementHttpClient.cs:47-92` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs:117-124`, `src/ZB.MOM.WW.ScadaBridge.CLI/ManagementHttpClient.cs:47-92` |
|
||||
|
||||
**Resolution (2026-05-28):** Replaced the `Convert.FromBase64String(...)` + `File.WriteAllBytes(...)` pair with a new `StreamBase64ToFile(base64, output)` helper that slices the base64 string into 4-char-aligned chunks (1 MB by default) and decodes each chunk straight into a `FileStream` via `Convert.TryFromBase64Chars`. The intermediate `byte[]` and the synchronous full-bundle write are gone — peak working set drops from ~base64-string + ~byte[] + ~envelope-string to ~base64-string + small-chunk-buffer + ~envelope-string. The response body is still buffered into the management envelope string (the `POST /management` wire format does not currently support streaming responses — this finding is bounded by that limit per the recommendation's stop-gap), so a streaming `POST /api/bundle/export` REST endpoint remains a follow-up. Regression tests `BundleCommandsStreamingTests` (6 tests) cover small payload round-trip, multi-chunk boundaries, empty input, invalid base64 → `FormatException`, and argument validation.
|
||||
|
||||
@@ -932,7 +932,7 @@ _Unresolved._
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/Commands/BundleCommands.cs:117-126` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs:117-126` |
|
||||
|
||||
**Resolution (2026-05-28):** Wrapped the `JsonDocument.Parse` + `GetProperty` extraction in a `try/catch (JsonException or KeyNotFoundException or InvalidOperationException)` block and the `StreamBase64ToFile` call in a separate `try/catch (FormatException)`. Either failure now emits a clean `OutputFormatter.WriteError(..., "INVALID_RESPONSE")` and returns exit 1, matching the graceful-degradation pattern established by CLI-002 / CLI-003 / CLI-005. A malformed/abbreviated envelope no longer terminates the CLI with a raw stack trace.
|
||||
|
||||
@@ -968,9 +968,9 @@ regression test against a malformed-envelope stub `HttpMessageHandler`.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CLI/CliConfig.cs:41-53` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CLI/CliConfig.cs:41-53` |
|
||||
|
||||
**Resolution (2026-05-28):** Wrapped the `File.ReadAllText` + `JsonSerializer.Deserialize` calls in a `try/catch` for `JsonException`/`IOException`/`UnauthorizedAccessException` that prints one warning to `Console.Error` and falls through with default values, so command-line and env-var precedence still works against a malformed `~/.scadalink/config.json`. Regression test `CliConfigTests.Load_MalformedConfigFile_DoesNotThrow_WarnsAndReturnsDefault` redirects `HOME`/`USERPROFILE` to a temp dir containing invalid JSON, asserts no throw, defaulted values, and the stderr warning.
|
||||
**Resolution (2026-05-28):** Wrapped the `File.ReadAllText` + `JsonSerializer.Deserialize` calls in a `try/catch` for `JsonException`/`IOException`/`UnauthorizedAccessException` that prints one warning to `Console.Error` and falls through with default values, so command-line and env-var precedence still works against a malformed `~/.scadabridge/config.json`. Regression test `CliConfigTests.Load_MalformedConfigFile_DoesNotThrow_WarnsAndReturnsDefault` redirects `HOME`/`USERPROFILE` to a temp dir containing invalid JSON, asserts no throw, defaulted values, and the stderr warning.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -987,7 +987,7 @@ if (File.Exists(configPath))
|
||||
}
|
||||
```
|
||||
|
||||
Neither call is guarded. If `~/.scadalink/config.json` exists but is malformed
|
||||
Neither call is guarded. If `~/.scadabridge/config.json` exists but is malformed
|
||||
(stale, partial, or someone's `vim` swap), `JsonSerializer.Deserialize` throws
|
||||
`JsonException`. If the file exists but isn't readable (mode 0000),
|
||||
`File.ReadAllText` throws `UnauthorizedAccessException`. Either fault aborts every
|
||||
@@ -1000,7 +1000,7 @@ input on the command line and don't need the config file at all (`--url`,
|
||||
Wrap the file-read and the `JsonSerializer.Deserialize` in a single
|
||||
`try/catch (Exception)` (or specifically `JsonException` +
|
||||
`UnauthorizedAccessException` + `IOException`). On failure, write a single one-line
|
||||
warning to `Console.Error` ("ignoring malformed `~/.scadalink/config.json`: {message}")
|
||||
warning to `Console.Error` ("ignoring malformed `~/.scadabridge/config.json`: {message}")
|
||||
and return the default `CliConfig`, so the rest of the precedence chain (env vars +
|
||||
command-line flags) still works.
|
||||
|
||||
@@ -1015,7 +1015,7 @@ _Unresolved._
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.CLI.Tests/CommandTreeTests.cs:21-37`, `:55-58` (vs. `src/ScadaLink.CLI/Program.cs:21-36`) |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/CommandTreeTests.cs:21-37`, `:55-58` (vs. `src/ZB.MOM.WW.ScadaBridge.CLI/Program.cs:21-36`) |
|
||||
|
||||
**Resolution (2026-05-28):** Added `AuditCommands.Build` and `BundleCommands.Build` to `AllCommandGroups()`, bumped the count assertion to `Equal(16, …)` with a maintenance comment, and added three new sub-command-surface tests (`AllCommandGroups_Contains_AuditAndBundle`, `AuditCommandGroup_HasQueryExportAndVerifyChain`, `BundleCommandGroup_HasExportPreviewAndImport`). `CommandPayloadTypes_ResolveViaRegistry` now also pins `ExportBundleCommand` / `PreviewBundleCommand` / `ImportBundleCommand` through `ManagementCommandRegistry`.
|
||||
|
||||
@@ -1047,13 +1047,13 @@ add a `BundleCommandsTests` file covering the success-envelope parse and the
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `docs/requirements/Component-CLI.md:310-311` (vs. `src/ScadaLink.CLI/Commands/AuditQueryHelpers.cs:186`, `src/ScadaLink.CLI/Commands/AuditExportHelpers.cs:126`, `src/ScadaLink.CLI/ManagementHttpClient.cs:94-156`) |
|
||||
| Location | `docs/requirements/Component-CLI.md:310-311` (vs. `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditQueryHelpers.cs:186`, `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditExportHelpers.cs:126`, `src/ZB.MOM.WW.ScadaBridge.CLI/ManagementHttpClient.cs:94-156`) |
|
||||
|
||||
**Resolution (2026-05-28):** Updated `Component-CLI.md` Dependencies bullets — the Management Service (#18) bullet now says the `scadalink audit` group rides a parallel REST surface (`GET /api/audit/query` / `GET /api/audit/export`) sharing HTTP Basic Auth with `/management` but bypassing the actor; the Audit Log (#23) bullet names the specific endpoints and the server-side `AuditEndpoints` permission enforcement (`OperationalAudit` / `AuditExport`).
|
||||
**Resolution (2026-05-28):** Updated `Component-CLI.md` Dependencies bullets — the Management Service (#18) bullet now says the `scadabridge audit` group rides a parallel REST surface (`GET /api/audit/query` / `GET /api/audit/export`) sharing HTTP Basic Auth with `/management` but bypassing the actor; the Audit Log (#23) bullet names the specific endpoints and the server-side `AuditEndpoints` permission enforcement (`OperationalAudit` / `AuditExport`).
|
||||
|
||||
**Description**
|
||||
|
||||
`Component-CLI.md:310` states: "The `scadalink audit` command group rides this same
|
||||
`Component-CLI.md:310` states: "The `scadabridge audit` command group rides this same
|
||||
transport — there is no separate audit endpoint." But the implementation calls a
|
||||
new REST surface — `GET /api/audit/query` and `GET /api/audit/export` — via two new
|
||||
methods on `ManagementHttpClient` (`SendGetAsync`, `SendGetStreamAsync`), distinct
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.CentralUI` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.CentralUI` |
|
||||
| Design doc | `docs/requirements/Component-CentralUI.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -131,7 +131,7 @@ a UX/design adherence gap), and the un-tested `TransportImport` /
|
||||
| Severity | Critical |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:171-424` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:171-424` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -184,7 +184,7 @@ the commit whose message references `CentralUI-001`.
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Auth/AuthEndpoints.cs:63-69`; `src/ScadaLink.CentralUI/Components/Pages/Deployment/*.razor` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/AuthEndpoints.cs:63-69`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/*.razor` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -231,7 +231,7 @@ message references `CentralUI-002`.
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:359-423` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:359-423` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -276,7 +276,7 @@ pre-fix code and pass after. Fixed by the commit whose message references
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Auth/CookieAuthenticationStateProvider.cs:22-28` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/CookieAuthenticationStateProvider.cs:22-28` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -324,7 +324,7 @@ Fixed by the commit whose message references `CentralUI-004`.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Auth/AuthEndpoints.cs:47-81`; `src/ScadaLink.CentralUI/Components/Shared/SessionExpiry.razor:18-30` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/AuthEndpoints.cs:47-81`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/SessionExpiry.razor:18-30` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -346,7 +346,7 @@ fixed 30-minute model. The code and the documented decision must agree.
|
||||
|
||||
Resolved 2026-05-16 (commit `<pending>`) — cross-module fix (CentralUI +
|
||||
Security), explicitly authorized. Root cause confirmed against the source:
|
||||
`AddCookie` (`ScadaLink.Security/ServiceCollectionExtensions.cs`) set neither
|
||||
`AddCookie` (`ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs`) set neither
|
||||
`ExpireTimeSpan` nor `SlidingExpiration`; `AuthEndpoints` stamped a fixed
|
||||
`expires_at = UtcNow + 30 min` claim and a 30-minute absolute cookie
|
||||
`ExpiresUtc`; `SessionExpiry.razor` scheduled one hard redirect at that fixed
|
||||
@@ -383,8 +383,8 @@ Regression tests fail against the pre-fix code and pass after. Security:
|
||||
`AddSecurity_AuthCookie_ExpireTimeSpanIsConfigurable` (pins the options-pattern
|
||||
binding). CentralUI: `SessionExpiryPolicyTests.BuildSignInProperties_DoesNotSetFixedAbsoluteExpiry`,
|
||||
`..._IsPersistent`, `..._AllowsSlidingRefresh` pin that the login sign-in no
|
||||
longer imposes a fixed absolute cap. `dotnet build ScadaLink.slnx` clean;
|
||||
`tests/ScadaLink.Security.Tests` 57 passed, `tests/ScadaLink.CentralUI.Tests`
|
||||
longer imposes a fixed absolute cap. `dotnet build ZB.MOM.WW.ScadaBridge.slnx` clean;
|
||||
`tests/ZB.MOM.WW.ScadaBridge.Security.Tests` 57 passed, `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests`
|
||||
254 passed.
|
||||
|
||||
### CentralUI-006 — Deployment status page polls every 10s despite the documented SignalR-push design
|
||||
@@ -394,7 +394,7 @@ longer imposes a fixed absolute cap. `dotnet build ScadaLink.slnx` clean;
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Deployment/Deployments.razor:196-216` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Deployments.razor:196-216` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -424,7 +424,7 @@ deployment records (`GetAllDeploymentRecordsAsync`) and the full instance map
|
||||
(`GetAllInstancesAsync`) — contradicting Component-CentralUI "Real-Time Updates"
|
||||
("transitions push to the UI immediately via SignalR … no polling required").
|
||||
|
||||
**Process/DI topology confirmed.** `ScadaLink.Host/Program.cs` calls both
|
||||
**Process/DI topology confirmed.** `ZB.MOM.WW.ScadaBridge.Host/Program.cs` calls both
|
||||
`AddDeploymentManager()` (line 75) and `AddCentralUI()` (line 77) on the same
|
||||
`builder.Services` — DeploymentManager and the Central UI run **in the same
|
||||
central Host process**, so a DI singleton is genuinely shared between the
|
||||
@@ -432,7 +432,7 @@ DeploymentManager services and the Blazor circuit's scoped components. The
|
||||
shared-singleton seam is real; no out-of-process fallback was needed.
|
||||
|
||||
**What was implemented — push-based updates.** A new
|
||||
`IDeploymentStatusNotifier` (`ScadaLink.DeploymentManager/IDeploymentStatusNotifier.cs`)
|
||||
`IDeploymentStatusNotifier` (`ZB.MOM.WW.ScadaBridge.DeploymentManager/IDeploymentStatusNotifier.cs`)
|
||||
with a C# `event Action<DeploymentStatusChange>` and a small payload
|
||||
(`DeploymentStatusChange` = deployment id + instance id + new status). Its
|
||||
implementation `DeploymentStatusNotifier` invokes each subscriber in isolation
|
||||
@@ -464,9 +464,9 @@ Regression tests fail against the pre-fix code and pass after. DeploymentManager
|
||||
pins the shared-singleton seam. CentralUI (`DeploymentsPushUpdateTests`):
|
||||
`Deployments_DoesNotPoll_HasNoRefreshTimer` (pre-fix: the `_refreshTimer` field
|
||||
existed — confirmed failing), `Deployments_StatusChange_TriggersReload`, and
|
||||
`Deployments_Dispose_UnsubscribesFromNotifier`. `dotnet build ScadaLink.slnx`
|
||||
clean (0 warnings); `tests/ScadaLink.DeploymentManager.Tests` 76 passed,
|
||||
`tests/ScadaLink.CentralUI.Tests` 257 passed. (`TopologyPageTests`' DI fixture
|
||||
`Deployments_Dispose_UnsubscribesFromNotifier`. `dotnet build ZB.MOM.WW.ScadaBridge.slnx`
|
||||
clean (0 warnings); `tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests` 76 passed,
|
||||
`tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests` 257 passed. (`TopologyPageTests`' DI fixture
|
||||
was also updated to register the new notifier, since it constructs the real
|
||||
`DeploymentService`.)
|
||||
|
||||
@@ -477,7 +477,7 @@ was also updated to register the new notifier, since it constructs the real
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor:69-78`; `src/ScadaLink.CentralUI/Components/Pages/Monitoring/EventLogs.razor:2`; `src/ScadaLink.CentralUI/Components/Pages/Monitoring/ParkedMessages.razor:2` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor:69-78`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/EventLogs.razor:2`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/ParkedMessages.razor:2` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -520,7 +520,7 @@ fail against the pre-fix code and pass after;
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Monitoring/AuditLog.razor:242-243` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AuditLog.razor:242-243` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -563,7 +563,7 @@ time-range filters, so it is unaffected.
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Deployment/DebugView.razor:400-409,538-544` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor:400-409,538-544` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -608,7 +608,7 @@ mechanism rather than the race window.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Shared/ToastNotification.razor:62-71,90` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/ToastNotification.razor:62-71,90` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -651,7 +651,7 @@ still-works behaviours.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Shared/DiffDialog.razor:89-95,151-157` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/DiffDialog.razor:89-95,151-157` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -687,7 +687,7 @@ path.
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Admin/Sites.razor:196-205` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/Sites.razor:196-205` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -722,7 +722,7 @@ LoadData_GroupsConnectionsBySite_AndRendersThem}` fail against the pre-fix code
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:951-952` (actual call at `:975`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:951-952` (actual call at `:975`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -764,7 +764,7 @@ source). The five existing `Hover`/`SignatureHelp` tests in
|
||||
| Severity | Low (re-triaged from Medium 2026-05-16 — see Resolution) |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:254-259`; `src/ScadaLink.CentralUI/ScriptAnalysis/SandboxHostHelpers.cs:26-117` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:254-259`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxHostHelpers.cs:26-117` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -814,7 +814,7 @@ cannot silently regress.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Won't Fix |
|
||||
| Location | `src/ScadaLink.CentralUI/ServiceCollectionExtensions.cs:24`; `src/ScadaLink.CentralUI/Components/Shared/DialogService.cs:18-69` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs:24`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/DialogService.cs:18-69` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -860,7 +860,7 @@ service cannot silently regress.
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Shared/DataTable.razor:62-68`; `src/ScadaLink.CentralUI/Components/Pages/Deployment/Deployments.razor:167-173` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/DataTable.razor:62-68`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Deployments.razor:167-173` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -898,7 +898,7 @@ includes first/last) and `PagerWindowTests` (6 tests pinning the helper logic).
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Auth/AuthEndpoints.cs:127-138` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/AuthEndpoints.cs:127-138` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -939,7 +939,7 @@ the pre-auth login exemption was not over-corrected.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Shared/MonacoEditor.razor:116-118,123,142,164,170,176,182,189`; `src/ScadaLink.CentralUI/Components/Shared/TreeView.razor:129,139`; `src/ScadaLink.CentralUI/Components/Pages/Admin/Sites.razor:316-319` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/MonacoEditor.razor:116-118,123,142,164,170,176,182,189`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor:129,139`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Admin/Sites.razor:316-319` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -991,7 +991,7 @@ not logged).
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.CentralUI.Tests/` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1028,7 +1028,7 @@ Toast/timer disposal: `ToastNotificationTests` (from CentralUI-010).
|
||||
This batch also added `BrowserTimeTests`, `MonitoringAuthorizationTests`,
|
||||
`SitesPageTests`, `DataTablePagerTests` + `PagerWindowTests`,
|
||||
`TreeViewStorageResilienceTests`, and `MonacoEditorLoggingTests`. The
|
||||
`tests/ScadaLink.CentralUI.Tests` suite is green at 251 tests. Remaining
|
||||
`tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests` suite is green at 251 tests. Remaining
|
||||
untested paths are low-risk render-only pages; the Critical/High/Medium paths
|
||||
the finding prioritised are all now covered, so the finding is considered
|
||||
resolved. (Note: `TopologyPageTests`'s DI setup was also updated this session —
|
||||
@@ -1044,7 +1044,7 @@ in the fixture.)
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Shared/SessionExpiry.razor:39-62`; `src/ScadaLink.CentralUI/Auth/CookieAuthenticationStateProvider.cs:29-43` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/SessionExpiry.razor:39-62`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/CookieAuthenticationStateProvider.cs:29-43` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1094,7 +1094,7 @@ expired session (see CentralUI-025).
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Deployment/DebugView.razor:404-419,511-519,275-289` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor:404-419,511-519,275-289` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1133,7 +1133,7 @@ critical section as the upsert.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Deployment/Deployments.razor:221-229,317-322` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/Deployments.razor:221-229,317-322` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1172,7 +1172,7 @@ rather than the whole table on each event.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Monitoring/ParkedMessages.razor:690-698`; `src/ScadaLink.CentralUI/Components/Shared/DiffDialog.razor:107-116,118-130,104` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/ParkedMessages.razor:690-698`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/DiffDialog.razor:107-116,118-130,104` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1205,11 +1205,11 @@ call, consistent with the CentralUI-018 fixes in the same module.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor:102`; `src/ScadaLink.CentralUI/Components/Pages/Dashboard.razor:14`; `GetCurrentUserAsync` in `Templates.razor`, `TemplateEdit.razor`, `TemplateCreate.razor`, `SharedScripts.razor`, `SharedScriptForm.razor`, `Sites.razor`, `Topology.razor`, `InstanceCreate.razor`, `InstanceConfigure.razor` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Layout/NavMenu.razor:102`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Dashboard.razor:14`; `GetCurrentUserAsync` in `Templates.razor`, `TemplateEdit.razor`, `TemplateCreate.razor`, `SharedScripts.razor`, `SharedScriptForm.razor`, `Sites.razor`, `Topology.razor`, `InstanceCreate.razor`, `InstanceConfigure.razor` |
|
||||
|
||||
**Description**
|
||||
|
||||
`ScadaLink.Security.JwtTokenService` exposes the canonical claim-type constants
|
||||
`ZB.MOM.WW.ScadaBridge.Security.JwtTokenService` exposes the canonical claim-type constants
|
||||
(`UsernameClaimType = "Username"`, `DisplayNameClaimType = "DisplayName"`,
|
||||
`RoleClaimType`, `SiteIdClaimType`). `SiteScopeService` correctly uses
|
||||
`JwtTokenService.SiteIdClaimType`, but every `GetCurrentUserAsync` helper across
|
||||
@@ -1239,7 +1239,7 @@ or a small scoped service) so the claim lookup lives in exactly one place.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.CentralUI.Tests/Auth/SessionExpiryPolicyTests.cs`; `src/ScadaLink.CentralUI/Components/Shared/SessionExpiry.razor` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Auth/SessionExpiryPolicyTests.cs`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/SessionExpiry.razor` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1273,7 +1273,7 @@ also forces the CentralUI-020 fix.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Audit/AuditFilterBar.razor:97-104`; `src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs:56-58,150-178,203-213` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditFilterBar.razor:97-104`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditQueryModel.cs:56-58,150-178,203-213` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1317,7 +1317,7 @@ in the same time zone in every documented deployment.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor:74-80`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:421-425`; `src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationReport.razor:75-81,639-640`; `src/ScadaLink.CentralUI/Components/Pages/Monitoring/EventLogs.razor:62-73,261-262` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor:74-80`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:421-425`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Notifications/NotificationReport.razor:75-81,639-640`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/EventLogs.razor:62-73,261-262` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1364,7 +1364,7 @@ changes.
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationReport.razor:2,434,472,502`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor:2,52-59`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:97-110,201,250-251,278-279` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Notifications/NotificationReport.razor:2,434,472,502`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor:2,52-59`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:97-110,201,250-251,278-279` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1420,7 +1420,7 @@ Regression test `SiteCallsReportPageTests.SiteScoping_ScopedDeploymentUser_Hides
|
||||
seeds a Deployment user with a single `SiteId=1` claim, asserts only the Plant-A row
|
||||
renders, and verifies the Plant-B row is dropped (the page's row count drops from 2 to
|
||||
1). All three existing report-page test fixtures register `SiteScopeService` so the
|
||||
default system-wide path is unaffected — the full `ScadaLink.CentralUI.Tests` suite
|
||||
default system-wide path is unaffected — the full `ZB.MOM.WW.ScadaBridge.CentralUI.Tests` suite
|
||||
still passes (568 / 568).
|
||||
|
||||
### CentralUI-029 — `ConfigurationAuditLog` uses `JS.InvokeAsync<int>("eval", ...)` instead of a dedicated JS module
|
||||
@@ -1430,9 +1430,9 @@ still passes (568 / 568).
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Audit/ConfigurationAuditLog.razor:248-263` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Audit/ConfigurationAuditLog.razor:248-263` |
|
||||
|
||||
**Resolution (2026-05-28):** Added a small 5-line `wwwroot/js/browser-time.js` ES module exporting `getTimezoneOffsetMinutes()`, and replaced the `JS.InvokeAsync<int>("eval", "new Date().getTimezoneOffset()")` call in `ConfigurationAuditLog.OnAfterRenderAsync` with a lazy `IJSObjectReference` import (`./_content/ScadaLink.CentralUI/js/browser-time.js`) + `module.InvokeAsync<int>("getTimezoneOffsetMinutes")`, matching the `session-expiry.js` / `audit-grid.js` / `nav-state.js` / `transport.js` module-import pattern. The residual `eval` JS-interop surface is gone and the page is now CSP-compatible with `unsafe-eval` forbidden.
|
||||
**Resolution (2026-05-28):** Added a small 5-line `wwwroot/js/browser-time.js` ES module exporting `getTimezoneOffsetMinutes()`, and replaced the `JS.InvokeAsync<int>("eval", "new Date().getTimezoneOffset()")` call in `ConfigurationAuditLog.OnAfterRenderAsync` with a lazy `IJSObjectReference` import (`./_content/ZB.MOM.WW.ScadaBridge.CentralUI/js/browser-time.js`) + `module.InvokeAsync<int>("getTimezoneOffsetMinutes")`, matching the `session-expiry.js` / `audit-grid.js` / `nav-state.js` / `transport.js` module-import pattern. The residual `eval` JS-interop surface is gone and the page is now CSP-compatible with `unsafe-eval` forbidden.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1464,7 +1464,7 @@ plumbing CentralUI-027 will need.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/ScriptAnalysis/SandboxConsoleCapture.cs:31-118`; `src/ScadaLink.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:401-404` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxConsoleCapture.cs:31-118`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs:401-404` |
|
||||
|
||||
**Resolution (2026-05-28):** Wrapped every `Write`/`WriteLine` override in `SandboxConsoleCapture` through a `WriteSynchronized` helper that takes a `lock` on the current `AsyncLocal` capture buffer before writing — concurrent `Console.WriteLine` calls from a script's `Task.WhenAll`/`Task.Run` fan-out now serialise on the buffer instance, so the `StringBuilder` underneath can no longer be corrupted. The fall-through to the unwrapped `_fallback` writer is unlocked because the BCL's process-wide `Console.Out` is already synchronised. Different capture scopes have different lock targets, so two unrelated sandbox runs never block each other. New regression test `SandboxConsoleCaptureTests.BeginCapture_ConcurrentWritesFromTasks_DoNotCorruptBuffer` drives 32 tasks × 50 lines each through one capture scope and asserts every line is intact in the buffer.
|
||||
|
||||
@@ -1501,9 +1501,9 @@ the expected line count regardless of thread interleaving.
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs:72,104-142,160-161` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor.cs:72,104-142,160-161` |
|
||||
|
||||
**Resolution (2026-05-28):** Replaced the `private byte[]? _bundleBytes` field with `private string? _bundleTempPath`. `OnFileSelectedAsync` now creates `Path.GetTempPath()/scadalink-transport-staging/` (created on first use) and streams the upload via `InputFile.OpenReadStream(maxBytes).CopyToAsync(FileStream)` straight to a `Guid.NewGuid():N + .scadabundle` temp file; `TryLoadAsync` opens the same path as a fresh `FileStream` for each `IBundleImporter.LoadAsync` call. The component now implements `IDisposable` and a `DeleteBundleTempFile()` helper that runs on `ResetSessionState`, `OnFileSelectedAsync` (before a new upload), and `Dispose` (circuit teardown); IO failures during cleanup are swallowed so audit-failure-style defensive semantics hold. Per-circuit working set drops from up to `MaxBundleSizeMb` (default 100 MB) per open wizard to the 80 KB FileStream buffer. The existing reflection-based test helper `SeedAtPassphraseStep` was migrated to write bytes to a real temp file and set `_bundleTempPath`, so the 7 existing TransportImport bUnit tests still pass against the new staging model.
|
||||
**Resolution (2026-05-28):** Replaced the `private byte[]? _bundleBytes` field with `private string? _bundleTempPath`. `OnFileSelectedAsync` now creates `Path.GetTempPath()/scadabridge-transport-staging/` (created on first use) and streams the upload via `InputFile.OpenReadStream(maxBytes).CopyToAsync(FileStream)` straight to a `Guid.NewGuid():N + .scadabundle` temp file; `TryLoadAsync` opens the same path as a fresh `FileStream` for each `IBundleImporter.LoadAsync` call. The component now implements `IDisposable` and a `DeleteBundleTempFile()` helper that runs on `ResetSessionState`, `OnFileSelectedAsync` (before a new upload), and `Dispose` (circuit teardown); IO failures during cleanup are swallowed so audit-failure-style defensive semantics hold. Per-circuit working set drops from up to `MaxBundleSizeMb` (default 100 MB) per open wizard to the 80 KB FileStream buffer. The existing reflection-based test helper `SeedAtPassphraseStep` was migrated to write bytes to a real temp file and set `_bundleTempPath`, so the 7 existing TransportImport bUnit tests still pass against the new staging model.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1536,7 +1536,7 @@ docs to call out the in-memory cost per concurrent import session.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor:76-82`; `src/ScadaLink.CentralUI/Components/Audit/AuditResultsGrid.razor.cs:65,196-197,219-220` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditResultsGrid.razor:76-82`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Audit/AuditResultsGrid.razor.cs:65,196-197,219-220` |
|
||||
|
||||
**Resolution (2026-05-28):** Added a `Stack<AuditLogPaging?> _cursorStack` and `AuditLogPaging? _currentPaging` field to `AuditResultsGrid.razor.cs`. `NextPage` now pushes the current cursor before advancing; a new `PrevPage` method pops the prior cursor, reloads at that position, and decrements `_pageNumber` only if the reload succeeds (a failed fetch leaves the user on the current page rather than stranding them between pages). The filter-change reset clears the stack alongside `_rows`. The razor template now renders a `btn-group` with a Previous button (gated on `CanGoBack`) alongside the existing Next button; both buttons get the standard `disabled` treatment during loads.
|
||||
|
||||
@@ -1568,9 +1568,9 @@ forward-only paging on the Audit Log grid.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs:97-238,267-319`; `src/ScadaLink.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:107-148`; `tests/ScadaLink.CentralUI.Tests/Pages/Design/TransportImportPageTests.cs`; `tests/ScadaLink.CentralUI.Tests/Pages/SiteCallsReportPageTests.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor.cs:97-238,267-319`; `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/SiteCalls/SiteCallsReport.razor.cs:107-148`; `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/Design/TransportImportPageTests.cs`; `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/SiteCallsReportPageTests.cs` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `tests/ScadaLink.CentralUI.Tests/Pages/QueryStringDrillInTests.cs` (4 bUnit tests). For `SiteCallsReport` it pins the case-insensitive `?status=parked` → canonical "Parked" normalisation, the unrecognised-status silent drop, and the non-boolean `?stuck=yes` silent drop — gaps the existing `SiteCallsReportPageTests` (which covered the Parked / stuck=true / no-params happy paths) did not exercise. For `TransportImport` it asserts that the wizard has no `[Parameter]`-bound query keys: an unrecognised drill-in URL (`?bundleImportId=…&foo=bar`) leaves `_step` at `Upload` and the Step-1 InputFile control renders cleanly.
|
||||
**Resolution (2026-05-28):** Added `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/QueryStringDrillInTests.cs` (4 bUnit tests). For `SiteCallsReport` it pins the case-insensitive `?status=parked` → canonical "Parked" normalisation, the unrecognised-status silent drop, and the non-boolean `?stuck=yes` silent drop — gaps the existing `SiteCallsReportPageTests` (which covered the Parked / stuck=true / no-params happy paths) did not exercise. For `TransportImport` it asserts that the wizard has no `[Parameter]`-bound query keys: an unrecognised drill-in URL (`?bundleImportId=…&foo=bar`) leaves `_step` at `Upload` and the Step-1 InputFile control renders cleanly.
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.ClusterInfrastructure` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` |
|
||||
| Design doc | `docs/requirements/Component-ClusterInfrastructure.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -40,7 +40,7 @@ well-documented, and well-tested. This re-review examined all three source files
|
||||
all three test files against the full 10-category checklist and found **two new
|
||||
issues**, both stemming from work the prior review explicitly deferred to a "Host
|
||||
review" that has not happened: the `DownIfAlone` property is exposed and validated as
|
||||
part of the configuration contract but is never consumed — `ScadaLink.Host`'s
|
||||
part of the configuration contract but is never consumed — `ZB.MOM.WW.ScadaBridge.Host`'s
|
||||
`BuildHocon` still hard-codes `down-if-alone = on` (CI-009, Medium) — and the validator
|
||||
does not enforce the design doc's requirement that `down-if-alone` be `on` for the
|
||||
keep-oldest resolver, so `DownIfAlone = false` is silently accepted (CI-010, Low).
|
||||
@@ -58,9 +58,9 @@ either did not surface or that have aged into the file:
|
||||
- **CI-011 (Low, Code organization)** — `ClusterOptions.SectionName` is
|
||||
documented as "the single source of truth so binding sites do not hard-code
|
||||
the magic string" (the very justification CI-005's resolution offered), but
|
||||
`ScadaLink.Host.SiteServiceRegistration.BindSharedOptions:100` and three
|
||||
references in `ScadaLink.Host.StartupValidator` all hard-code
|
||||
`"ScadaLink:Cluster"` literals. The constant is decorative — a "single source
|
||||
`ZB.MOM.WW.ScadaBridge.Host.SiteServiceRegistration.BindSharedOptions:100` and three
|
||||
references in `ZB.MOM.WW.ScadaBridge.Host.StartupValidator` all hard-code
|
||||
`"ScadaBridge:Cluster"` literals. The constant is decorative — a "single source
|
||||
of truth" that nothing reads. Same pattern as CI-009 (inert configuration knob).
|
||||
- **CI-012 (Low, Design-document adherence)** — the validator accepts
|
||||
`SeedNodes.Count == 1` even though the design doc states "both nodes are seed
|
||||
@@ -90,7 +90,7 @@ Original review (2026-05-16, `9c60592`) below; the re-review notes (2026-05-17,
|
||||
| # | Category | Examined | Notes |
|
||||
|---|----------|----------|-------|
|
||||
| 1 | Correctness & logic bugs | ✓ | No executable logic exists beyond an options POCO; no logic bugs, but `ServiceCollectionExtensions` returns success while doing nothing (CI-002). **Re-review:** CI-002 resolved. New — `DownIfAlone` is a settable property that controls nothing because the HOCON builder hard-codes the value (CI-009). |
|
||||
| 2 | Akka.NET conventions | ✓ | No actors, no `ActorSystem` bootstrap, no supervision, no cluster/singleton wiring exist despite the design doc requiring all of them (CI-001). Nothing to assess against `Tell`/`Ask`, immutability, or `PipeTo`. **Re-review:** confirmed the Akka bootstrap legitimately lives in `ScadaLink.Host` (CI-001 resolution); still nothing actor-related in this module. No issues. |
|
||||
| 2 | Akka.NET conventions | ✓ | No actors, no `ActorSystem` bootstrap, no supervision, no cluster/singleton wiring exist despite the design doc requiring all of them (CI-001). Nothing to assess against `Tell`/`Ask`, immutability, or `PipeTo`. **Re-review:** confirmed the Akka bootstrap legitimately lives in `ZB.MOM.WW.ScadaBridge.Host` (CI-001 resolution); still nothing actor-related in this module. No issues. |
|
||||
| 3 | Concurrency & thread safety | ✓ | No shared mutable state, no actors, no async code. No issues found in current code. **Re-review:** validator and DI extensions are stateless; no issues. |
|
||||
| 4 | Error handling & resilience | ✓ | Failover, split-brain, dual-node recovery, and graceful-shutdown logic are entirely absent (CI-001). No exception paths to review in current code. **Re-review:** the validator now fails fast on misconfiguration. New — it does not enforce the design doc's `down-if-alone = on` requirement (CI-010). |
|
||||
| 5 | Security | ✓ | No authn/authz surface in this module. Akka remoting is unconfigured, so transport security cannot be assessed; flagged as part of the missing implementation (CI-001). No secret handling present. **Re-review:** still no authn/authz surface, no secret handling. No issues. |
|
||||
@@ -124,7 +124,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:9`, `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:9`, `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:16` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -138,8 +138,8 @@ and a `ServiceCollectionExtensions` whose methods are explicitly commented
|
||||
and simply return the unmodified `IServiceCollection`. There is no `Akka.Cluster`,
|
||||
`Akka.Cluster.Tools`, `Akka.Remote`, or split-brain-resolver dependency in the
|
||||
`.csproj` at all (it references only `Microsoft.Extensions.DependencyInjection.Abstractions`,
|
||||
`Microsoft.Extensions.Options`, and `ScadaLink.Commons`). Because every other
|
||||
ScadaLink component runs inside the actor system this module is responsible for
|
||||
`Microsoft.Extensions.Options`, and `ZB.MOM.WW.ScadaBridge.Commons`). Because every other
|
||||
ScadaBridge component runs inside the actor system this module is responsible for
|
||||
creating, the absence of any implementation blocks the foundational layer of the
|
||||
system.
|
||||
|
||||
@@ -157,19 +157,19 @@ should clearly state it is unimplemented so callers do not assume otherwise.
|
||||
_Re-triaged 2026-05-16 — remains Open, needs a design decision from the user._
|
||||
|
||||
Verified against the source at the reviewed commit: the finding's factual claims hold.
|
||||
`src/ScadaLink.ClusterInfrastructure` still contains only `ClusterOptions.cs` and a
|
||||
`src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` still contains only `ClusterOptions.cs` and a
|
||||
no-op `ServiceCollectionExtensions.cs`, and the `.csproj` references no Akka packages.
|
||||
|
||||
However, the documented cluster behaviour is **not actually absent from the system** —
|
||||
it has been implemented in the **Host** project rather than in this module:
|
||||
|
||||
- `src/ScadaLink.Host/Actors/AkkaHostedService.cs` bootstraps the `ActorSystem`,
|
||||
generates the HOCON from `ClusterOptions` (it imports `ScadaLink.ClusterInfrastructure`
|
||||
- `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs` bootstraps the `ActorSystem`,
|
||||
generates the HOCON from `ClusterOptions` (it imports `ZB.MOM.WW.ScadaBridge.ClusterInfrastructure`
|
||||
and injects `IOptions<ClusterOptions>`), and configures the `keep-oldest` split-brain
|
||||
resolver with `down-if-alone = on` (see `AkkaHostedService.cs:95-96`).
|
||||
- `src/ScadaLink.Host/Health/AkkaClusterHealthCheck.cs`, `AkkaClusterNodeProvider.cs`,
|
||||
- `src/ZB.MOM.WW.ScadaBridge.Host/Health/AkkaClusterHealthCheck.cs`, `AkkaClusterNodeProvider.cs`,
|
||||
and `Health/ActiveNodeHealthCheck.cs` cover cluster membership / active-node detection.
|
||||
- Akka cluster/remote package references live in `ScadaLink.Host.csproj` and the
|
||||
- Akka cluster/remote package references live in `ZB.MOM.WW.ScadaBridge.Host.csproj` and the
|
||||
per-component projects (`SiteRuntime`, `Communication`, etc.).
|
||||
|
||||
So the real situation is an **ownership / design-doc drift**, not missing behaviour:
|
||||
@@ -183,11 +183,11 @@ of two substantial decisions, both requiring the user:
|
||||
|
||||
1. **Move the bootstrap into this module** — relocate the HOCON generation, split-brain
|
||||
config, cluster-singleton helpers and `CoordinatedShutdown` wiring out of
|
||||
`ScadaLink.Host` into `ScadaLink.ClusterInfrastructure`, add the Akka package
|
||||
`ZB.MOM.WW.ScadaBridge.Host` into `ZB.MOM.WW.ScadaBridge.ClusterInfrastructure`, add the Akka package
|
||||
references, and re-wire the Host to call into it. This is a cross-module refactor
|
||||
touching `src/ScadaLink.Host/*` and several other projects — outside the edit scope
|
||||
permitted for this finding (only `src/ScadaLink.ClusterInfrastructure/`,
|
||||
`tests/ScadaLink.ClusterInfrastructure.Tests/`, and this file may be edited).
|
||||
touching `src/ZB.MOM.WW.ScadaBridge.Host/*` and several other projects — outside the edit scope
|
||||
permitted for this finding (only `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/`,
|
||||
`tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/`, and this file may be edited).
|
||||
2. **Accept the current placement** — keep the bootstrap in the Host and update
|
||||
`Component-ClusterInfrastructure.md` (and the README component table) to record that
|
||||
the Host owns the actor-system/cluster bootstrap and that this module's role is the
|
||||
@@ -202,8 +202,8 @@ bring-up), and the design docs are corrected to record the true ownership.
|
||||
**Resolved** — fixing commit `<pending>`, date 2026-05-16. The finding was a design-doc
|
||||
drift, not missing behaviour. `docs/requirements/Component-ClusterInfrastructure.md` now
|
||||
carries an "Implementation Note — Code Placement" section stating that the
|
||||
`ScadaLink.ClusterInfrastructure` project owns the `ClusterOptions` configuration model
|
||||
while `ScadaLink.Host` owns the Akka bootstrap, HOCON generation, split-brain-resolver
|
||||
`ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` project owns the `ClusterOptions` configuration model
|
||||
while `ZB.MOM.WW.ScadaBridge.Host` owns the Akka bootstrap, HOCON generation, split-brain-resolver
|
||||
wiring, `CoordinatedShutdown` integration, and active-node health checks. The README
|
||||
component table (row 13) was updated to match. No code change was required — the
|
||||
documented cluster behaviour already exists and is exercised; only the doc's
|
||||
@@ -216,7 +216,7 @@ module-ownership claim was wrong. Module test suite green (3 passed).
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:7-17` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:7-17` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -239,17 +239,17 @@ with the genuine registration when CI-001 is addressed.
|
||||
**Resolution**
|
||||
|
||||
Confirmed against the source: both methods returned the `IServiceCollection`
|
||||
unchanged. Verified the consumers — `ScadaLink.Host` calls `AddClusterInfrastructure()`
|
||||
unchanged. Verified the consumers — `ZB.MOM.WW.ScadaBridge.Host` calls `AddClusterInfrastructure()`
|
||||
(`Program.cs:68`, `SiteServiceRegistration.cs:24`); `AddClusterInfrastructureActors`
|
||||
is dead — it is called nowhere in the solution.
|
||||
|
||||
**Resolved** — fixing commit `commit pending`, date 2026-05-16.
|
||||
`AddClusterInfrastructure` now does real work: it registers the
|
||||
`ClusterOptionsValidator` (CI-004) via `TryAddEnumerable`, so the method is no longer a
|
||||
no-op and a misconfigured `ScadaLink:Cluster` section fails fast on the first
|
||||
no-op and a misconfigured `ScadaBridge:Cluster` section fails fast on the first
|
||||
`IOptions<ClusterOptions>` resolution. `AddClusterInfrastructureActors` — which this
|
||||
component never had any actors to register, as CI-001 established the Akka bootstrap
|
||||
lives in `ScadaLink.Host` — now throws `NotImplementedException` with a message
|
||||
lives in `ZB.MOM.WW.ScadaBridge.Host` — now throws `NotImplementedException` with a message
|
||||
pointing the caller to the Host, rather than masquerading as a completed registration.
|
||||
Covered by `ServiceCollectionExtensionsTests`
|
||||
(`AddClusterInfrastructure_RegistersOptionsValidator`,
|
||||
@@ -263,7 +263,7 @@ Covered by `ServiceCollectionExtensionsTests`
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:3-11` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:3-11` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -290,7 +290,7 @@ agree on where each value lives.
|
||||
**Resolution**
|
||||
|
||||
Partially re-triaged. Verified against the source: most of the "missing" settings are
|
||||
**deliberately owned by `ScadaLink.Host.NodeOptions`** — `NodeOptions` already carries
|
||||
**deliberately owned by `ZB.MOM.WW.ScadaBridge.Host.NodeOptions`** — `NodeOptions` already carries
|
||||
`Role`, `NodeHostname`, `SiteId`, `RemotingPort` and `GrpcPort`, and `AkkaHostedService`
|
||||
builds the HOCON from `NodeOptions` for exactly those values. Local SQLite storage paths
|
||||
live in the database / store-and-forward options. This is the ownership split CI-001
|
||||
@@ -307,7 +307,7 @@ deliberate ownership split — node identity/remoting/gRPC in `Host.NodeOptions`
|
||||
paths in the database options, cluster-formation settings here — so the design doc and
|
||||
the options classes now agree on where each value lives. (`AkkaHostedService` currently
|
||||
hard-codes `down-if-alone = on` in HOCON; wiring it to read `DownIfAlone` is a one-line
|
||||
`ScadaLink.Host` change, outside this module's permitted edit scope, and is noted for
|
||||
`ZB.MOM.WW.ScadaBridge.Host` change, outside this module's permitted edit scope, and is noted for
|
||||
the Host's review.) Covered by `ClusterOptionsTests.DefaultValues_AreCorrect` and
|
||||
`ClusterOptionsTests.DownIfAlone_CanBeSet`.
|
||||
|
||||
@@ -318,7 +318,7 @@ the Host's review.) Covered by `ClusterOptionsTests.DefaultValues_AreCorrect` an
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:3-11` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:3-11` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -374,7 +374,7 @@ attributes cannot. Covered by `ClusterOptionsValidatorTests` (8 cases) and
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:3` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:3` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -398,9 +398,9 @@ Confirmed against the source: `ClusterOptions` previously exposed no section-nam
|
||||
constant, leaving binding sites to hard-code the magic string.
|
||||
|
||||
**Resolved** — fixing commit `commit pending`, date 2026-05-16. `ClusterOptions` now
|
||||
exposes `public const string SectionName = "ScadaLink:Cluster";` as the single source
|
||||
exposes `public const string SectionName = "ScadaBridge:Cluster";` as the single source
|
||||
of truth for the `appsettings.json` section name, with an XML doc explaining its
|
||||
purpose. The chosen value matches the `ScadaLink:`-prefixed section convention used by
|
||||
purpose. The chosen value matches the `ScadaBridge:`-prefixed section convention used by
|
||||
peer option classes and referenced by `ClusterOptionsValidator` / the design doc.
|
||||
Covered by `ClusterOptionsTests.SectionName_IsTheExpectedAppSettingsSection`, which
|
||||
both pins the value and — by referencing the constant — guards against its removal
|
||||
@@ -413,7 +413,7 @@ both pins the value and — by referencing the constant — guards against its r
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.ClusterInfrastructure.Tests/ClusterOptionsTests.cs:1-51` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsTests.cs:1-51` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -438,14 +438,14 @@ from `ClusterOptions` and for the options validation from CI-004.
|
||||
**Resolution**
|
||||
|
||||
Re-triaged in light of CI-001's resolution. The Akka bootstrap, HOCON generation,
|
||||
cluster formation, failover and singleton handover are owned by `ScadaLink.Host`, not
|
||||
cluster formation, failover and singleton handover are owned by `ZB.MOM.WW.ScadaBridge.Host`, not
|
||||
this project — multi-node `Akka.Cluster.TestKit` tests for that behaviour belong in the
|
||||
Host's test suite, outside this module's scope. What this module legitimately owns is
|
||||
`ClusterOptions`, its validator, and the DI registration, and the testing gap there is
|
||||
now closed.
|
||||
|
||||
**Resolved** — fixing commit `commit pending`, date 2026-05-16. Added two test classes
|
||||
to `tests/ScadaLink.ClusterInfrastructure.Tests`: `ClusterOptionsValidatorTests`
|
||||
to `tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests`: `ClusterOptionsValidatorTests`
|
||||
(8 cases — valid defaults pass; `MinNrOfMembers != 1`, unsupported split-brain
|
||||
strategies, empty seed nodes, heartbeat not below the failure threshold, non-positive
|
||||
`StableAfter` all fail; and a multi-failure accumulation case) and
|
||||
@@ -467,7 +467,7 @@ design); `ClusterOptionsValidator` is the layer that now rejects `keep-majority`
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:3-11` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:3-11` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -510,7 +510,7 @@ inspection of `ClusterOptions.cs:3-74`. Module test suite green (17 passed).
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:9`, `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:9`, `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:16` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -540,17 +540,17 @@ complete-looking design doc with no caveat. That premise has been overtaken by t
|
||||
CI-001/CI-002 work:
|
||||
|
||||
- The "Phase 0 skeleton" comments no longer exist anywhere in
|
||||
`src/ScadaLink.ClusterInfrastructure` (verified by `grep`). `ServiceCollectionExtensions`
|
||||
`src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` (verified by `grep`). `ServiceCollectionExtensions`
|
||||
now does real work (registers `ClusterOptionsValidator`) and `AddClusterInfrastructureActors`
|
||||
throws explicitly — both with accurate XML docs explaining the ownership split.
|
||||
- The module is no longer an unimplemented skeleton. CI-001 established that the Akka
|
||||
bootstrap legitimately lives in `ScadaLink.Host`, and this project's true scope —
|
||||
bootstrap legitimately lives in `ZB.MOM.WW.ScadaBridge.Host`, and this project's true scope —
|
||||
the `ClusterOptions` configuration contract, its validator, and DI registration — is
|
||||
fully implemented and tested.
|
||||
- The design doc `Component-ClusterInfrastructure.md` now opens with an
|
||||
"Implementation Note — Code Placement" section (added by CI-001) that explicitly
|
||||
states the component is a *design responsibility* realised across
|
||||
`ScadaLink.ClusterInfrastructure` (configuration model) and `ScadaLink.Host`
|
||||
`ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` (configuration model) and `ZB.MOM.WW.ScadaBridge.Host`
|
||||
(bootstrap/runtime wiring), and the README component table (row 13) was updated to
|
||||
match. A reader of the design doc no longer assumes a single fully-built project.
|
||||
|
||||
@@ -569,7 +569,7 @@ inspection of `ServiceCollectionExtensions.cs` and
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:74` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:74` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -577,7 +577,7 @@ The `DownIfAlone` property was added to `ClusterOptions` by CI-003's resolution
|
||||
part of "the split-brain configuration contract". It is public, defaults to `true`,
|
||||
carries an XML doc presenting it as "the design-doc requirement", and is exercised by
|
||||
`ClusterOptionsTests.DownIfAlone_CanBeSet`. However, nothing in the system reads it.
|
||||
The Akka.NET HOCON is generated by `ScadaLink.Host.Actors.AkkaHostedService.BuildHocon`,
|
||||
The Akka.NET HOCON is generated by `ZB.MOM.WW.ScadaBridge.Host.Actors.AkkaHostedService.BuildHocon`,
|
||||
which **hard-codes** the resolver setting:
|
||||
|
||||
```
|
||||
@@ -596,7 +596,7 @@ field (`SeedNodes`, `MinNrOfMembers`, `SplitBrainResolverStrategy`, `StableAfter
|
||||
The result is a configuration property that an operator can set in `appsettings.json`,
|
||||
that passes validation, and that has **zero runtime effect** — setting
|
||||
`DownIfAlone: false` does not turn the flag off. CI-003's resolution explicitly
|
||||
acknowledged this gap ("wiring it to read `DownIfAlone` is a one-line `ScadaLink.Host`
|
||||
acknowledged this gap ("wiring it to read `DownIfAlone` is a one-line `ZB.MOM.WW.ScadaBridge.Host`
|
||||
change ... noted for the Host's review") but the wiring was never done and no tracked
|
||||
finding carried it, so the gap has silently persisted to commit `39d737e`. An inert,
|
||||
misleadingly-documented configuration knob is a correctness and design-adherence
|
||||
@@ -616,13 +616,13 @@ controls nothing.
|
||||
**Resolution**
|
||||
|
||||
Root cause verified against the source at commit `39d737e`:
|
||||
`src/ScadaLink.Host/Actors/AkkaHostedService.cs:147` hard-codes `down-if-alone = on`
|
||||
`src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:147` hard-codes `down-if-alone = on`
|
||||
inside the `keep-oldest` block, and `BuildHocon` consumes every other `ClusterOptions`
|
||||
field but never reads `clusterOptions.DownIfAlone`. The finding's facts hold. The fix
|
||||
is correctly scoped to the **Host** module — the configuration property and its
|
||||
validation legitimately live in `ScadaLink.ClusterInfrastructure` (this module),
|
||||
validation legitimately live in `ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` (this module),
|
||||
and the per-CLAUDE.md ownership split (CI-001) places HOCON generation in
|
||||
`ScadaLink.Host`.
|
||||
`ZB.MOM.WW.ScadaBridge.Host`.
|
||||
|
||||
**Resolved** — by cross-reference, date 2026-05-17. No code change in this module:
|
||||
`ClusterOptions.DownIfAlone` and its validation (CI-010) are correct and complete here.
|
||||
@@ -638,7 +638,7 @@ green (18 passed).
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptionsValidator.cs:21-71` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs:21-71` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -688,27 +688,27 @@ confirmed failing, then passing after the fix. Module test suite green (18 passe
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:24-27`, `src/ScadaLink.Host/SiteServiceRegistration.cs:100`, `src/ScadaLink.Host/StartupValidator.cs:43`, `src/ScadaLink.Host/StartupValidator.cs:75` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:24-27`, `src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs:100`, `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:43`, `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:75` |
|
||||
|
||||
**Resolution (2026-05-28):** Took option (b) since wiring the constant into the Host's `SiteServiceRegistration.BindSharedOptions` / `StartupValidator` is outside this module's editable surface — deleted the `SectionName` constant from `ClusterOptions.cs` and the companion `SectionName_IsTheExpectedAppSettingsSection` test from `ClusterOptionsTests.cs`. The Host's `"ScadaLink:Cluster"` literals now stand alone (consistent with the implementation rather than the broken "single source of truth" claim). A code-comment placeholder records the rationale so a future Host-side change can reinstate the constant alongside the binding-site updates.
|
||||
**Resolution (2026-05-28):** Took option (b) since wiring the constant into the Host's `SiteServiceRegistration.BindSharedOptions` / `StartupValidator` is outside this module's editable surface — deleted the `SectionName` constant from `ClusterOptions.cs` and the companion `SectionName_IsTheExpectedAppSettingsSection` test from `ClusterOptionsTests.cs`. The Host's `"ScadaBridge:Cluster"` literals now stand alone (consistent with the implementation rather than the broken "single source of truth" claim). A code-comment placeholder records the rationale so a future Host-side change can reinstate the constant alongside the binding-site updates.
|
||||
|
||||
**Description**
|
||||
|
||||
`ClusterOptions.SectionName` was added by CI-005 as `public const string SectionName =
|
||||
"ScadaLink:Cluster";`, with an XML doc declaring it "the single source of truth so
|
||||
"ScadaBridge:Cluster";`, with an XML doc declaring it "the single source of truth so
|
||||
binding sites do not hard-code the magic string". CI-005's resolution likewise framed
|
||||
the constant as the canonical reference value. In practice, **no caller in the
|
||||
solution reads it**. `grep -rn "ClusterOptions.SectionName" src/` returns zero hits.
|
||||
Every site that needs the section name hard-codes the literal:
|
||||
|
||||
- `ScadaLink.Host.SiteServiceRegistration.BindSharedOptions:100` —
|
||||
`services.Configure<ClusterOptions>(config.GetSection("ScadaLink:Cluster"));`
|
||||
- `ScadaLink.Host.StartupValidator:43,45,75` — three `"ScadaLink:Cluster"` /
|
||||
`"ScadaLink:Cluster:SeedNodes"` literals.
|
||||
- `ZB.MOM.WW.ScadaBridge.Host.SiteServiceRegistration.BindSharedOptions:100` —
|
||||
`services.Configure<ClusterOptions>(config.GetSection("ScadaBridge:Cluster"));`
|
||||
- `ZB.MOM.WW.ScadaBridge.Host.StartupValidator:43,45,75` — three `"ScadaBridge:Cluster"` /
|
||||
`"ScadaBridge:Cluster:SeedNodes"` literals.
|
||||
|
||||
The `SectionName_IsTheExpectedAppSettingsSection` test pins the constant's value but
|
||||
does not protect against the underlying drift hazard: if someone changes
|
||||
`SectionName` to `"ScadaLink:Akka:Cluster"`, the test still passes (because it tests
|
||||
`SectionName` to `"ScadaBridge:Akka:Cluster"`, the test still passes (because it tests
|
||||
the constant against the same literal), the validator still registers, and binding
|
||||
silently goes to whichever string the Host hard-codes. The constant currently
|
||||
provides none of the safety its XML doc claims. This is the same pattern of "inert
|
||||
@@ -717,7 +717,7 @@ configuration drift rather than runtime behaviour.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Either (a) replace the hard-coded `"ScadaLink:Cluster"` literals in
|
||||
Either (a) replace the hard-coded `"ScadaBridge:Cluster"` literals in
|
||||
`SiteServiceRegistration.cs:100` and `StartupValidator.cs:43,45,75` with
|
||||
`ClusterOptions.SectionName` (a small Host-module change, to be tracked there), or
|
||||
(b) if the constant is intentionally decorative, soften the XML doc so it does not
|
||||
@@ -731,7 +731,7 @@ guarantee the code does not deliver.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptionsValidator.cs:30-43` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs:30-43` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -741,18 +741,18 @@ guarantee the code does not deliver.
|
||||
> its partner. Either node can start first and form the cluster; the other joins when
|
||||
> it starts. No startup ordering dependency.
|
||||
|
||||
A correctly-configured ScadaLink deployment therefore lists **two** seed nodes.
|
||||
A correctly-configured ScadaBridge deployment therefore lists **two** seed nodes.
|
||||
`ClusterOptionsValidator.Validate` only checks that `SeedNodes` is non-null and
|
||||
non-empty (`Count == 0`). A configuration with a single seed node passes validation
|
||||
silently — but that defeats the "no startup ordering dependency" guarantee the
|
||||
design doc explicitly calls out.
|
||||
|
||||
`ScadaLink.Host.StartupValidator:43-46` does enforce the rule:
|
||||
`ZB.MOM.WW.ScadaBridge.Host.StartupValidator:43-46` does enforce the rule:
|
||||
|
||||
```csharp
|
||||
var seedNodes = configuration.GetSection("ScadaLink:Cluster:SeedNodes").Get<List<string>>();
|
||||
var seedNodes = configuration.GetSection("ScadaBridge:Cluster:SeedNodes").Get<List<string>>();
|
||||
if (seedNodes is null || seedNodes.Count < 2)
|
||||
errors.Add("ScadaLink:Cluster:SeedNodes must have at least 2 entries");
|
||||
errors.Add("ScadaBridge:Cluster:SeedNodes must have at least 2 entries");
|
||||
```
|
||||
|
||||
So the rule is enforced — but by the **other** project, after the
|
||||
@@ -789,7 +789,7 @@ ClusterInfrastructure.Tests).
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.ClusterInfrastructure.Tests/ClusterOptionsTests.cs:47-67` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsTests.cs:47-67` |
|
||||
|
||||
**Resolution (2026-05-28):** Added a 10-line inline `// ClusterInfra-013: ...` block at the top of `Properties_CanBeSetToCustomValues` explicitly recording that this test exercises the POCO property setters only — the `keep-majority` strategy and `MinNrOfMembers = 2` values are explicitly forbidden in production by `ClusterOptionsValidator`, and the comment cross-references `UnsupportedSplitBrainStrategy_FailsValidation` and `MinNrOfMembers_NotOne_FailsValidation` so a future reader cannot misread the test as endorsing those values.
|
||||
|
||||
@@ -828,7 +828,7 @@ goal is to make the test's intent self-documenting.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:42-48` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:42-48` |
|
||||
|
||||
**Resolution (2026-05-28):** Deleted the `AddClusterInfrastructureActors` extension method from `ServiceCollectionExtensions.cs` and its companion `AddClusterInfrastructureActors_ThrowsRatherThanSilentlySucceeding` test from `ServiceCollectionExtensionsTests.cs`. Verified no production caller existed before deletion via `grep -rn`. A code comment records the rationale (CI-001 ownership question now permanently settled; method served only to throw and was IDE-auto-complete noise). The class-level XML doc on the test file was updated to drop the stale reference to the removed test.
|
||||
|
||||
@@ -840,7 +840,7 @@ and a body that unconditionally throws `NotImplementedException`. CI-002's resol
|
||||
chose "throw loudly" over "delete" specifically because CI-001 was still resolving the
|
||||
ownership-split question. That question is settled — the design doc, the README
|
||||
component table, and `Component-ClusterInfrastructure.md`'s "Implementation Note — Code
|
||||
Placement" all permanently locate the Akka actor bootstrap in `ScadaLink.Host`.
|
||||
Placement" all permanently locate the Akka actor bootstrap in `ZB.MOM.WW.ScadaBridge.Host`.
|
||||
|
||||
A `grep -rn "AddClusterInfrastructureActors" src/ tests/` confirms there is no caller
|
||||
anywhere in the solution. The method's only consumer is its own test
|
||||
@@ -854,6 +854,6 @@ expecting it to register something), and gives nothing in return.
|
||||
Delete `AddClusterInfrastructureActors`, delete its test, and add a one-line note to
|
||||
`docs/requirements/Component-ClusterInfrastructure.md`'s code-placement section
|
||||
explicitly stating that this project exposes no actor-registration extension
|
||||
(actor wiring lives in `ScadaLink.Host`). If the user prefers to keep the
|
||||
(actor wiring lives in `ZB.MOM.WW.ScadaBridge.Host`). If the user prefers to keep the
|
||||
"fail-fast" trap, mark the method `[Obsolete(true, error: true)]` so the compiler —
|
||||
not the runtime — rejects the call.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.Commons` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.Commons` |
|
||||
| Design doc | `docs/requirements/Component-Commons.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -79,8 +79,8 @@ coverage for the new types is uneven — `TrackedOperationId`, `SiteCallOperatio
|
||||
`Notification`, and `SiteCall` are all directly tested; the Transport types
|
||||
(`BundleManifest`, `EncryptionMetadata`, `BundleSession`, `BundleSummary`, `ExportSelection`,
|
||||
`ImportPreview`, `ImportResolution`, `ImportResult`, `ManifestContentEntry`) have only
|
||||
integration-level coverage in `tests/ScadaLink.Transport.IntegrationTests/`, with no
|
||||
shape/serialization tests in `ScadaLink.Commons.Tests`.
|
||||
integration-level coverage in `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/`, with no
|
||||
shape/serialization tests in `ZB.MOM.WW.ScadaBridge.Commons.Tests`.
|
||||
|
||||
## Checklist coverage
|
||||
|
||||
@@ -109,7 +109,7 @@ shape/serialization tests in `ScadaLink.Commons.Tests`.
|
||||
| 6 | Performance & resource management | ✓ | `IBundleSessionStore.EvictExpired` exists for sessions — good. `BundleSession` carries `DecryptedContent` plus `Manifest` per session; the size is bounded by the configured bundle cap but no explicit per-session size accounting. `ExternalCallResult.Response` lazy parse not thread-safe (Commons-021). |
|
||||
| 7 | Design-document adherence | ✓ | `Component-Commons.md` is now significantly stale relative to the actual file set: stale enum values for `AuditKind`/`AuditStatus`, missing `AuditEvent`/`SiteCall` entities, missing `IAuditLogRepository`, missing six service interfaces and `Interfaces/Transport/`, missing four `Types/*` folders and `Messages/Audit/` (Commons-017). |
|
||||
| 8 | Code organization & conventions | ✓ | `IOperationTrackingStore` and `IPartitionMaintenance` live at the root of `Interfaces/` rather than under `Interfaces/Services/` (Commons-018). `BundleSession.Locked` uses a magic `3` rather than a named constant (Commons-016). Message contracts and entities otherwise follow the additive-evolution / POCO / `record` conventions. |
|
||||
| 9 | Testing coverage | ✓ | Transport types (`BundleManifest`, `EncryptionMetadata`, `BundleSession`, `BundleSummary`, `ExportSelection`, `ImportPreview`, `ImportResolution`, `ImportResult`, `ManifestContentEntry`) have no unit tests in `tests/ScadaLink.Commons.Tests/`; only `tests/ScadaLink.Transport.IntegrationTests/` exercises them (Commons-020). `IngestAuditEventsCommand` / `IngestCachedTelemetryCommand` / `UpsertSiteCallCommand` / `PullAuditEventsRequest` / `PullAuditEventsResponse` / `AuditTelemetryEnvelope` shape tests also absent. |
|
||||
| 9 | Testing coverage | ✓ | Transport types (`BundleManifest`, `EncryptionMetadata`, `BundleSession`, `BundleSummary`, `ExportSelection`, `ImportPreview`, `ImportResolution`, `ImportResult`, `ManifestContentEntry`) have no unit tests in `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/`; only `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/` exercises them (Commons-020). `IngestAuditEventsCommand` / `IngestCachedTelemetryCommand` / `UpsertSiteCallCommand` / `PullAuditEventsRequest` / `PullAuditEventsResponse` / `AuditTelemetryEnvelope` shape tests also absent. |
|
||||
| 10 | Documentation & comments | ✓ | `IAuditCorrelationContext` references `BundleImporter.ApplyAsync` — an implementation type Commons does not see, so the `<see cref>` is unresolvable (Commons-022b, folded into Commons-022). `ImportPreviewItem.FieldDiffJson` and `Notification.ResolvedTargets` are JSON-string columns with no documented shape contract (Commons-022). |
|
||||
|
||||
## Findings
|
||||
@@ -121,7 +121,7 @@ shape/serialization tests in `ScadaLink.Commons.Tests`.
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/StaleTagMonitor.cs:42-46`, `:62-67` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/StaleTagMonitor.cs:42-46`, `:62-67` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -162,7 +162,7 @@ internal `CallbackEnteredHook` test seam).
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/DynamicJsonElement.cs:10-17` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/DynamicJsonElement.cs:10-17` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -200,7 +200,7 @@ remarks block documenting the lifetime contract. Regression tests added in
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/ScriptParameters.cs:72-86` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/ScriptParameters.cs:72-86` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -241,11 +241,11 @@ contract is unchanged. Regression tests added in `ScriptParametersTests`
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Messages/Management/ManagementCommandRegistry.cs:14-35` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementCommandRegistry.cs:14-35` |
|
||||
|
||||
**Description**
|
||||
|
||||
`BuildRegistry` registers only types in the exact `ScadaLink.Commons.Messages.Management`
|
||||
`BuildRegistry` registers only types in the exact `ZB.MOM.WW.ScadaBridge.Commons.Messages.Management`
|
||||
namespace whose names end in `Command`. `GetCommandName(Type)`, however, strips a
|
||||
`Command` suffix from *any* type passed to it. The two halves disagree:
|
||||
|
||||
@@ -295,7 +295,7 @@ SiteRuntime all build clean against the change. Regression tests added in
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs:25-51` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/OpcUaEndpointConfigSerializer.cs:25-51` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -338,7 +338,7 @@ out-of-scope to change. Regression tests added in `OpcUaEndpointConfigSerializer
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/DynamicJsonElement.cs:47-51`, `:66-76` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/DynamicJsonElement.cs:47-51`, `:66-76` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -377,7 +377,7 @@ Regression tests added in `DynamicJsonElementTests` (`TryConvert_ObjectTarget_On
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/ScriptParameters.cs`, `src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs`, `src/ScadaLink.Commons/Validators/OpcUaEndpointConfigValidator.cs`, `src/ScadaLink.Commons/Types/StaleTagMonitor.cs`, `src/ScadaLink.Commons/Types/ScriptArgs.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/ScriptParameters.cs`, `src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/OpcUaEndpointConfigSerializer.cs`, `src/ZB.MOM.WW.ScadaBridge.Commons/Validators/OpcUaEndpointConfigValidator.cs`, `src/ZB.MOM.WW.ScadaBridge.Commons/Types/StaleTagMonitor.cs`, `src/ZB.MOM.WW.ScadaBridge.Commons/Types/ScriptArgs.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -423,7 +423,7 @@ carve-out makes these types compliant by design.
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Messages/Management/InstanceCommands.cs:10` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/InstanceCommands.cs:10` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -450,12 +450,12 @@ as `Item1`/`Item2`, unfriendly to REQ-COM-5a additive evolution. Introduced a na
|
||||
`ConnectionBinding(string AttributeName, int DataConnectionId)` in
|
||||
`Messages/Management/InstanceCommands.cs` (alongside the command, matching the existing
|
||||
record-per-file convention) and changed `SetConnectionBindingsCommand.Bindings` to
|
||||
`IReadOnlyList<ConnectionBinding>`. All consumers were updated in lock-step: `ScadaLink.CLI`
|
||||
`IReadOnlyList<ConnectionBinding>`. All consumers were updated in lock-step: `ZB.MOM.WW.ScadaBridge.CLI`
|
||||
(`InstanceCommands.TryParseBindings` now builds a `List<ConnectionBinding>`),
|
||||
`ScadaLink.TemplateEngine` (`InstanceService.SetConnectionBindingsAsync` parameter),
|
||||
`ScadaLink.ManagementService` (`ManagementActor` forwards `cmd.Bindings` unchanged — the
|
||||
`ZB.MOM.WW.ScadaBridge.TemplateEngine` (`InstanceService.SetConnectionBindingsAsync` parameter),
|
||||
`ZB.MOM.WW.ScadaBridge.ManagementService` (`ManagementActor` forwards `cmd.Bindings` unchanged — the
|
||||
new element type flows through), and one consumer the finding did not list,
|
||||
`ScadaLink.CentralUI` (`InstanceConfigure.razor`'s `SaveBindings` built a `List<(string,int)>`).
|
||||
`ZB.MOM.WW.ScadaBridge.CentralUI` (`InstanceConfigure.razor`'s `SaveBindings` built a `List<(string,int)>`).
|
||||
A repo-wide `src/` scan confirms no other references remain. Regression tests added in
|
||||
`ConnectionBindingSerializationTests` (`ConnectionBinding_SerializesWithNamedProperties`
|
||||
asserts named `AttributeName`/`DataConnectionId` JSON properties and the absence of
|
||||
@@ -463,7 +463,7 @@ asserts named `AttributeName`/`DataConnectionId` JSON properties and the absence
|
||||
JSON round-trip); existing parse/forward tests (`ParseBindings_ValidJson_ReturnsPairs`,
|
||||
`SetConnectionBindings_BulkAssignment_Success`) were updated to the new type. Affected
|
||||
suites are green (Commons 226, CLI 77, ManagementService 55, TemplateEngine 287) and
|
||||
`dotnet build ScadaLink.slnx` is clean.
|
||||
`dotnet build ZB.MOM.WW.ScadaBridge.slnx` is clean.
|
||||
|
||||
### Commons-009 — `Component-Commons.md` is stale relative to the actual file set
|
||||
|
||||
@@ -521,11 +521,11 @@ regression test.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.Commons.Tests/` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/` |
|
||||
|
||||
**Description**
|
||||
|
||||
`ScadaLink.Commons.Tests` covers `Result`, `RetryPolicy`, `ScriptParameters`,
|
||||
`ZB.MOM.WW.ScadaBridge.Commons.Tests` covers `Result`, `RetryPolicy`, `ScriptParameters`,
|
||||
`StaleTagMonitor`, the OPC UA validator, enums, message conventions, compatibility, and
|
||||
entity conventions. It does not cover several types that contain exactly the kind of
|
||||
edge-case logic that warrants tests:
|
||||
@@ -567,7 +567,7 @@ tests (up from 196).
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/Result.cs:15-20`, `:30-32`, `:36` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Result.cs:15-20`, `:30-32`, `:36` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -600,7 +600,7 @@ interpolated string, so no consumer is broken. Regression tests added in `Result
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/ValueFormatter.cs:20-27` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/ValueFormatter.cs:20-27` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -639,7 +639,7 @@ each pinned under `de-DE`).
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/DynamicJsonElement.cs:40-54` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/DynamicJsonElement.cs:40-54` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -682,7 +682,7 @@ tests added in `DynamicJsonElementTests` (`IndexAccess_WithLongIndex_Works`,
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs:107-131` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Serialization/OpcUaEndpointConfigSerializer.cs:107-131` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -733,7 +733,7 @@ describe the corrupt-typed-row branch. Regression tests added in
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/Transport/EncryptionMetadata.cs:3-8` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/EncryptionMetadata.cs:3-8` |
|
||||
|
||||
**Resolution (2026-05-28):** Converted `EncryptionMetadata` to a non-positional `record` with an explicit constructor that enforces invariants at the type boundary: `Algorithm` must equal `"AES-256-GCM"`, `Kdf` must equal `"PBKDF2-SHA256"`, `Iterations` must lie in `[MinPbkdf2Iterations=100_000, MaxPbkdf2Iterations=10_000_000]`, and `SaltB64`/`IvB64` must be non-null (empty permitted for the BundleSerializer.Pack seed pattern). Invalid values throw `ArgumentException` (or `ArgumentNullException`) naming the offending field. Added `EncryptionMetadataTests` covering valid construction, unknown algorithm/KDF (including case sensitivity), out-of-range iteration counts (including both boundaries), and null salt/IV. Updated `BundleSecretEncryptorTests` / `BundleSerializerTests` to use `MinPbkdf2Iterations` instead of the prior 10_000 placeholder.
|
||||
|
||||
@@ -785,7 +785,7 @@ accepted values on the record.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Types/Transport/BundleSession.cs:13-16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSession.cs:13-16` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `public const int MaxUnlockAttempts = 3;` to `BundleSession` with an XML doc cross-referencing the authoritative `TransportOptions.MaxUnlockAttemptsPerSession`. The `Locked` getter now reads `FailedUnlockAttempts >= MaxUnlockAttempts` instead of comparing against the literal `3`, and the property's XML doc names the constant. No call-site update required — the existing Transport-component `TransportOptions.MaxUnlockAttemptsPerSession` (also `3`) remains the operator-facing dial; this constant is the shim's own threshold, now searchable for a security review.
|
||||
|
||||
@@ -883,17 +883,17 @@ needed again now.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Interfaces/IOperationTrackingStore.cs`, `src/ScadaLink.Commons/Interfaces/IPartitionMaintenance.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/IOperationTrackingStore.cs`, `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/IPartitionMaintenance.cs` |
|
||||
|
||||
**Resolution (2026-05-28):** Moved both files into `src/ScadaLink.Commons/Interfaces/Services/`, matching the REQ-COM-5b sub-folder convention alongside the other service interfaces (`ISiteAuditQueue`, `INodeIdentityProvider`, `ICachedCallLifecycleObserver`, etc.). The 9 consumer files across `ScadaLink.SiteRuntime`, `ScadaLink.AuditLog`, `ScadaLink.ConfigurationDatabase`, and `ScadaLink.Host` exceed the in-instructions 8-file STOP threshold for namespace rewrites, so the namespace was deliberately kept as `ScadaLink.Commons.Interfaces` (not `.Services`) — no consumer change required, build remains green. A comment in each moved file records the rationale and notes that adopting the canonical `.Services` namespace can be picked up alongside any future Commons-wide namespace tidy-up.
|
||||
**Resolution (2026-05-28):** Moved both files into `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/`, matching the REQ-COM-5b sub-folder convention alongside the other service interfaces (`ISiteAuditQueue`, `INodeIdentityProvider`, `ICachedCallLifecycleObserver`, etc.). The 9 consumer files across `ZB.MOM.WW.ScadaBridge.SiteRuntime`, `ZB.MOM.WW.ScadaBridge.AuditLog`, `ZB.MOM.WW.ScadaBridge.ConfigurationDatabase`, and `ZB.MOM.WW.ScadaBridge.Host` exceed the in-instructions 8-file STOP threshold for namespace rewrites, so the namespace was deliberately kept as `ZB.MOM.WW.ScadaBridge.Commons.Interfaces` (not `.Services`) — no consumer change required, build remains green. A comment in each moved file records the rationale and notes that adopting the canonical `.Services` namespace can be picked up alongside any future Commons-wide namespace tidy-up.
|
||||
|
||||
**Description**
|
||||
|
||||
REQ-COM-5b documents the `Interfaces/` folder as having exactly three sub-folders:
|
||||
`Protocol/` (REQ-COM-2), `Repositories/` (REQ-COM-4), and `Services/` (REQ-COM-4a). Two
|
||||
new interfaces — `IOperationTrackingStore` and `IPartitionMaintenance` — are filed at
|
||||
the root of `Interfaces/` (namespace `ScadaLink.Commons.Interfaces`) rather than under
|
||||
`Interfaces/Services/` (namespace `ScadaLink.Commons.Interfaces.Services`). They are
|
||||
the root of `Interfaces/` (namespace `ZB.MOM.WW.ScadaBridge.Commons.Interfaces`) rather than under
|
||||
`Interfaces/Services/` (namespace `ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services`). They are
|
||||
straightforward cross-cutting service interfaces consumed by the Audit Log component (a
|
||||
site-local SQLite tracking store; a central partition-maintenance hosted-service helper)
|
||||
and conceptually belong alongside `ISiteAuditQueue`, `ICachedCallLifecycleObserver`, etc.
|
||||
@@ -904,8 +904,8 @@ other recently-added service interface uses `Interfaces.Services`.
|
||||
**Recommendation**
|
||||
|
||||
Move both files into `Interfaces/Services/` and adjust the namespace to
|
||||
`ScadaLink.Commons.Interfaces.Services`. Update consumers in `ScadaLink.AuditLog`,
|
||||
`ScadaLink.SiteRuntime`, and `ScadaLink.ConfigurationDatabase`. Add them to the
|
||||
`ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services`. Update consumers in `ZB.MOM.WW.ScadaBridge.AuditLog`,
|
||||
`ZB.MOM.WW.ScadaBridge.SiteRuntime`, and `ZB.MOM.WW.ScadaBridge.ConfigurationDatabase`. Add them to the
|
||||
REQ-COM-4a list (see Commons-017).
|
||||
|
||||
### Commons-019 — New `*Utc`-suffixed `DateTime` columns on `AuditEvent` / `SiteCall` are not enforced as UTC; inconsistent with `Notification`'s `DateTimeOffset`
|
||||
@@ -915,9 +915,9 @@ REQ-COM-4a list (see Commons-017).
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Entities/Audit/AuditEvent.cs:15-18`, `src/ScadaLink.Commons/Entities/Audit/SiteCall.cs:59-68`, `tests/ScadaLink.Commons.Tests/Entities/EntityConventionTests.cs:49-69` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Audit/AuditEvent.cs:15-18`, `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Audit/SiteCall.cs:59-68`, `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Entities/EntityConventionTests.cs:49-69` |
|
||||
|
||||
**Resolution (2026-05-28):** Kept the `DateTime` type on `AuditEvent` (a `DateTimeOffset` migration is a data-shape change beyond this finding's scope) and instead enforced the UTC invariant at the assignment boundary. `AuditEvent.OccurredAtUtc` / `IngestedAtUtc` now have init-setters that call `DateTime.SpecifyKind(value, DateTimeKind.Utc)` via private backing fields, so any value supplied with `Kind=Unspecified` (`DateTime` literal, JSON deserialise, EF hydrate that bypassed the converter) is re-tagged as UTC on assignment. The record-level XML doc gained a remarks block stating the invariant and contrasting with `Notification`'s `DateTimeOffset` shape. Sibling `ConfigurationDatabase-018` adds the matching EF value converter so the read path also enforces `Kind=Utc`; the two changes travelled together. Regression coverage in `tests/ScadaLink.ConfigurationDatabase.Tests/Configurations/AuditLogEntityTypeConfigurationTests.cs::Configure_UtcConverter_HydratesOccurredAtUtcAsKindUtc`. The `SiteCall` and `EntityConventionTests` sub-points named in the location list are out of scope for this close (they fall under sibling code-review tasks).
|
||||
**Resolution (2026-05-28):** Kept the `DateTime` type on `AuditEvent` (a `DateTimeOffset` migration is a data-shape change beyond this finding's scope) and instead enforced the UTC invariant at the assignment boundary. `AuditEvent.OccurredAtUtc` / `IngestedAtUtc` now have init-setters that call `DateTime.SpecifyKind(value, DateTimeKind.Utc)` via private backing fields, so any value supplied with `Kind=Unspecified` (`DateTime` literal, JSON deserialise, EF hydrate that bypassed the converter) is re-tagged as UTC on assignment. The record-level XML doc gained a remarks block stating the invariant and contrasting with `Notification`'s `DateTimeOffset` shape. Sibling `ConfigurationDatabase-018` adds the matching EF value converter so the read path also enforces `Kind=Utc`; the two changes travelled together. Regression coverage in `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Configurations/AuditLogEntityTypeConfigurationTests.cs::Configure_UtcConverter_HydratesOccurredAtUtcAsKindUtc`. The `SiteCall` and `EntityConventionTests` sub-points named in the location list are out of scope for this close (they fall under sibling code-review tasks).
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -967,16 +967,16 @@ Option 2 is the smaller change and is consistent with how `AuditLog` rows are st
|
||||
SQL Server (`datetime2`, no offset). Either way the inconsistency with `Notification`
|
||||
should be documented in REQ-COM-1 as a deliberate choice.
|
||||
|
||||
### Commons-020 — Transport types and new Audit-message types have no unit tests in `ScadaLink.Commons.Tests`
|
||||
### Commons-020 — Transport types and new Audit-message types have no unit tests in `ZB.MOM.WW.ScadaBridge.Commons.Tests`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.Commons.Tests/` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `tests/ScadaLink.Commons.Tests/Types/Transport/TransportRecordsTests.cs` (14 tests) covering ctor and System.Text.Json round-trip for `BundleManifest`, `ExportSelection`, `ImportPreview` + `ImportPreviewItem`, `ImportResolution` (all four `ResolutionAction`s), and `ImportResult`, plus a record-equality sanity check that catches a positional/tuple slip. `EncryptionMetadata` (Commons-015), `AuditEvent` (init-setter / `SourceNode`), `CachedCallTelemetry`, and `AuditTelemetryEnvelope` were verified already covered by their existing focused test files; no new tests were added for those to avoid duplication.
|
||||
**Resolution (2026-05-28):** Added `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/TransportRecordsTests.cs` (14 tests) covering ctor and System.Text.Json round-trip for `BundleManifest`, `ExportSelection`, `ImportPreview` + `ImportPreviewItem`, `ImportResolution` (all four `ResolutionAction`s), and `ImportResult`, plus a record-equality sanity check that catches a positional/tuple slip. `EncryptionMetadata` (Commons-015), `AuditEvent` (init-setter / `SourceNode`), `CachedCallTelemetry`, and `AuditTelemetryEnvelope` were verified already covered by their existing focused test files; no new tests were added for those to avoid duplication.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -984,8 +984,8 @@ The Transport (#24) work adds nine records under `Types/Transport/` (`BundleMani
|
||||
`EncryptionMetadata`, `BundleSession`, `BundleSummary`, `ExportSelection`,
|
||||
`ImportPreview` + `ImportPreviewItem`, `ImportResolution`, `ImportResult`,
|
||||
`ManifestContentEntry`) and four interfaces under `Interfaces/Transport/`. None of them
|
||||
have a focused test file in `tests/ScadaLink.Commons.Tests/` — coverage is entirely
|
||||
inside `tests/ScadaLink.Transport.IntegrationTests/`, which exercises the
|
||||
have a focused test file in `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/` — coverage is entirely
|
||||
inside `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/`, which exercises the
|
||||
end-to-end exporter/importer flow but does not pin the Commons-level wire contracts.
|
||||
|
||||
Similarly, the new `Messages/Audit/` folder (`IngestAuditEventsCommand`/`Reply`,
|
||||
@@ -1005,11 +1005,11 @@ regression.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add focused tests in `tests/ScadaLink.Commons.Tests/Types/Transport/` (round-trip
|
||||
Add focused tests in `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/` (round-trip
|
||||
serialization for each Transport record, named JSON property assertions for
|
||||
`EncryptionMetadata` / `BundleManifest`, the `BundleSession.Locked` threshold —
|
||||
see Commons-016, the `ConflictKind`/`ResolutionAction` enum coverage), and in
|
||||
`tests/ScadaLink.Commons.Tests/Messages/Audit/` (round-trip + named-property assertions
|
||||
`tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Messages/Audit/` (round-trip + named-property assertions
|
||||
for the seven new message files). Prioritise the contracts that cross the site→central
|
||||
boundary (`AuditTelemetryEnvelope`, `PullAuditEventsRequest`/`Response`,
|
||||
`IngestCachedTelemetryCommand`).
|
||||
@@ -1021,7 +1021,7 @@ boundary (`AuditTelemetryEnvelope`, `PullAuditEventsRequest`/`Response`,
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Interfaces/Services/IExternalSystemClient.cs:91-104` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/IExternalSystemClient.cs:91-104` |
|
||||
|
||||
**Resolution (2026-05-28):** Replaced the two mutable backing fields (`_response`/`_responseParsed`) with a single `private readonly Lazy<dynamic?> _response` initialised in the field initializer — `LazyThreadSafetyMode.ExecutionAndPublication` (the default) guarantees the parse runs at most once and every concurrent reader observes the same published `DynamicJsonElement`. `Response` is now a one-line `_response.Value` expression-bodied property. Regression test `ExternalCallResultTests.Response_ConcurrentReads_ReturnSameInstance` fires 64 concurrent readers through a `Barrier` and asserts `Assert.Same` across all observed values.
|
||||
|
||||
@@ -1074,7 +1074,7 @@ behavior.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Interfaces/Transport/IAuditCorrelationContext.cs:11`, `src/ScadaLink.Commons/Types/Transport/ImportPreview.cs:11`, `src/ScadaLink.Commons/Entities/Notifications/Notification.cs:33` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Transport/IAuditCorrelationContext.cs:11`, `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs:11`, `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/Notification.cs:33` |
|
||||
|
||||
**Resolution (2026-05-28):** Replaced the unresolvable `<see cref="BundleImporter.ApplyAsync"/>` in `IAuditCorrelationContext` with a plain-text `BundleImporter.ApplyAsync` reference (qualified inline as "in the Transport component") so the XML doc no longer emits a CS1574 warning from Commons, which cannot see the implementation type. The `ImportPreviewItem.FieldDiffJson` / `Notification.ResolvedTargets` JSON-shape sub-point is tracked separately and not in scope for this close — the XML doc on `IAuditCorrelationContext` does not name those columns.
|
||||
|
||||
@@ -1084,7 +1084,7 @@ Two related XML-doc weaknesses, both around the new Transport / Audit surface:
|
||||
|
||||
1. `IAuditCorrelationContext`'s remarks say
|
||||
`<see cref="BundleImporter.ApplyAsync"/>`. `BundleImporter` is the concrete
|
||||
implementation in `ScadaLink.Transport.Import`, which Commons does not (and must
|
||||
implementation in `ZB.MOM.WW.ScadaBridge.Transport.Import`, which Commons does not (and must
|
||||
not) reference. The cref is unresolvable from Commons and will surface as a
|
||||
build-time XML doc warning. The correct reference is the interface method
|
||||
`IBundleImporter.ApplyAsync`.
|
||||
@@ -1120,7 +1120,7 @@ Two related XML-doc weaknesses, both around the new Transport / Audit surface:
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Commons/Messages/Audit/SiteCallQueries.cs:53-66`, `:110-123`, `src/ScadaLink.Commons/Messages/Notification/NotificationOutboxQueries.cs:26-39`, `:104-123`, `src/ScadaLink.Commons/Types/SiteCallOperational.cs:42-54`, `src/ScadaLink.Commons/Types/TrackingStatusSnapshot.cs:33-46` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Audit/SiteCallQueries.cs:53-66`, `:110-123`, `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Notification/NotificationOutboxQueries.cs:26-39`, `:104-123`, `src/ZB.MOM.WW.ScadaBridge.Commons/Types/SiteCallOperational.cs:42-54`, `src/ZB.MOM.WW.ScadaBridge.Commons/Types/TrackingStatusSnapshot.cs:33-46` |
|
||||
|
||||
**Resolution (2026-05-28):** Read all six locations and confirmed the dominant pattern is "trailing-optional with `= null` default" (`SiteCallSummary`, `SiteCallDetail`, `NotificationSummary`, `NotificationDetail`, `NotificationOutboxQueryRequest.SourceNodeFilter`, `SiteCallQueryRequest.SourceNodeFilter` all already use this form). The single odd-one-out was `TrackingStatusSnapshot.SourceNode`, declared as `string? SourceNode` with no default — added the `= null` default to unify it with the rest. Verified both existing callers (`OperationTrackingStore.cs` and `TrackingApiTests.cs`) use named arguments, so the change is purely additive. `SiteCallOperational.SourceNode` sits in the middle of its positional parameter list rather than the trailing slot — that's a separate positional-record concern outside the "trailing-optional" pattern the finding called out, and moving it would touch many telemetry/proto consumers, so it was deliberately not touched here.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.Communication` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.Communication` |
|
||||
| Design doc | `docs/requirements/Component-Communication.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -107,7 +107,7 @@ Low.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/DebugStreamService.cs:130-143` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/DebugStreamService.cs:130-143` |
|
||||
|
||||
**Re-triaged 2026-05-16:** originally filed Critical, claiming an orphaned bridge actor
|
||||
and a multi-minute site-side resource leak on every snapshot timeout. On verification
|
||||
@@ -157,7 +157,7 @@ references `Communication-001`.
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/DebugStreamBridgeActor.cs:170`, `src/ScadaLink.Communication/Actors/DebugStreamBridgeActor.cs:143` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs:170`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs:143` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -200,7 +200,7 @@ fails against the pre-fix code and passes after.
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Grpc/SiteStreamGrpcClient.cs:77`, `src/ScadaLink.Communication/Grpc/SiteStreamGrpcClient.cs:106` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs:77`, `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs:106` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -246,7 +246,7 @@ fail against the pre-fix logic and pass after.
|
||||
| Severity | Medium |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:42`, `src/ScadaLink.Communication/Actors/SiteCommunicationActor.cs:22` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:42`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs:22` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -289,7 +289,7 @@ against the pre-fix code (decider yields `Restart`) and pass after.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Grpc/SiteStreamGrpcClient.cs:25`, `src/ScadaLink.Communication/CommunicationOptions.cs:36` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs:25`, `src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptions.cs:36` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -320,7 +320,7 @@ option and update the design doc.
|
||||
Resolved 2026-05-16 (commit pending). Root cause confirmed: `SiteStreamGrpcClient`
|
||||
hard-coded the keepalive values, `GrpcMaxStreamLifetime` was referenced nowhere, and
|
||||
`GrpcMaxConcurrentStreams` was never bound to the server. Fix (scoped to
|
||||
`src/ScadaLink.Communication`): `SiteStreamGrpcClient` gained a constructor taking
|
||||
`src/ZB.MOM.WW.ScadaBridge.Communication`): `SiteStreamGrpcClient` gained a constructor taking
|
||||
`CommunicationOptions` and now applies `GrpcKeepAlivePingDelay`/`GrpcKeepAlivePingTimeout`
|
||||
to its `SocketsHttpHandler`; `SiteStreamGrpcClientFactory` gained an
|
||||
`IOptions<CommunicationOptions>` DI constructor and flows the options into every client
|
||||
@@ -341,7 +341,7 @@ exercise the wiring (they require the new members to even compile).
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:204` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:204` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -380,7 +380,7 @@ against the pre-fix code and passes after.
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Grpc/SiteStreamGrpcClientFactory.cs:53` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClientFactory.cs:53` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -419,7 +419,7 @@ fails against the pre-fix code (clients disposed via `DisposeAsync`) and passes
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/DebugStreamBridgeActor.cs:71`, `src/ScadaLink.Communication/Actors/DebugStreamBridgeActor.cs:174` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs:71`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs:174` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -464,7 +464,7 @@ per-event reset (`Grpc_Error_Resets_RetryCount_On_Successful_Event`) was replace
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:53`, `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:240` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:53`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:240` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -503,7 +503,7 @@ registered) and passes after.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/DebugStreamBridgeActor.cs:10` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs:10` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -535,7 +535,7 @@ not derive from an Akka.Persistence base class; its state does not survive a res
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.Communication.Tests/` (module-wide) |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/` (module-wide) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -575,7 +575,7 @@ passes after):
|
||||
- Malformed `NodeAAddress` aborting `HandleSiteAddressCacheLoaded` (Communication-009) —
|
||||
`CentralCommunicationActorTests.MalformedSiteAddress_DoesNotAbortRefresh_OtherSitesStillRegistered`
|
||||
(added with this finding's resolution).
|
||||
The full module suite (`dotnet test tests/ScadaLink.Communication.Tests`) is green at
|
||||
The full module suite (`dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests`) is green at
|
||||
111 passing tests.
|
||||
|
||||
### Communication-012 — gRPC client factory ignores the endpoint on a cache hit, breaking NodeA→NodeB stream failover
|
||||
@@ -585,7 +585,7 @@ The full module suite (`dotnet test tests/ScadaLink.Communication.Tests`) is gre
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Grpc/SiteStreamGrpcClientFactory.cs:39`, `src/ScadaLink.Communication/Actors/DebugStreamBridgeActor.cs:166` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClientFactory.cs:39`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs:166` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -642,7 +642,7 @@ factory and pass after.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Grpc/SiteStreamGrpcClientFactory.cs:58` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClientFactory.cs:58` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -691,7 +691,7 @@ after.
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Grpc/SiteStreamGrpcServer.cs:124` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs:124` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -734,7 +734,7 @@ accepted.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.Communication.Tests/Grpc/DebugStreamBridgeActorTests.cs:401`, `tests/ScadaLink.Communication.Tests/Grpc/SiteStreamGrpcClientFactoryTests.cs` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/DebugStreamBridgeActorTests.cs:401`, `tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcClientFactoryTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -775,14 +775,14 @@ than being masked by an endpoint-agnostic mock.
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:169`, `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:338-375` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:169`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:338-375` |
|
||||
|
||||
**Resolution** — deleted the dead code path in favour of the keepalive-based
|
||||
detection that is the actual production behaviour: removed the
|
||||
`Receive<ConnectionStateChanged>` handler, the `HandleConnectionStateChanged`
|
||||
method, the `_debugSubscriptions` / `_inProgressDeployments` tracking dicts
|
||||
+ the `TrackMessageForCleanup` helper that fed them, and the dead message
|
||||
record `src/ScadaLink.Commons/Messages/Communication/ConnectionStateChanged.cs`.
|
||||
record `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Communication/ConnectionStateChanged.cs`.
|
||||
The two dead tests (`ConnectionLost_DebugStreamsKilled` in
|
||||
CentralCommunicationActorTests, `RoundTrip_ConnectionStateChanged_Succeeds`
|
||||
in CompatibilityTests) were removed alongside. The design doc
|
||||
@@ -858,7 +858,7 @@ Either way, replace `CentralCommunicationActorTests.ConnectionLost_DebugStreamsK
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:73`, `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:501`, `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:357-367` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:73`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:501`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:357-367` |
|
||||
|
||||
**Resolution (2026-05-28):** Closed by Comm-016 — field removed in commit ac96b83.
|
||||
The `_inProgressDeployments` dictionary, the `TrackMessageForCleanup` helper,
|
||||
@@ -917,7 +917,7 @@ caller, so the reply skips the coordinator.)
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/SiteCommunicationActor.cs:376-465` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs:376-465` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -966,7 +966,7 @@ Communication.Tests).
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:397-431` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:397-431` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1006,7 +1006,7 @@ the finding.
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:567` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:567` |
|
||||
|
||||
**Resolution (2026-05-28):** `SiteAddressCacheLoaded`'s `SiteContacts` payload is now typed as `IReadOnlyDictionary<string, IReadOnlyList<string>>`, enforcing the Akka.NET message-immutability convention at the type level rather than relying on producer discipline. The producer (`LoadSiteAddressesFromDb`) builds the working buckets as before and wraps each inner `List<string>` with `AsReadOnly()` before constructing the message — the freeze is local to the single refresh tick and the cost is negligible. The consumer (`HandleSiteAddressCacheLoaded`) only ever read via `Keys`, foreach-deconstruct, `Select`, `Count` and `ToImmutableHashSet`, all of which are supported by the new read-only types, so no consumer changes were needed. The existing `MalformedSiteAddress_DoesNotAbortRefresh_OtherSitesStillRegistered` and `ClusterClientRouting_RoutesToConfiguredSite` regression tests exercise the producer→consumer flow and continue to pass under the read-only types.
|
||||
|
||||
@@ -1037,7 +1037,7 @@ once per refresh tick.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Grpc/SiteStreamGrpcServer.cs:188-200` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs:188-200` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1086,7 +1086,7 @@ asserts `ActiveStreamCount == 0` and that `RemoveSubscriber` was NOT called
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:67`, `src/ScadaLink.Communication/Actors/CentralCommunicationActor.cs:493` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:67`, `src/ZB.MOM.WW.ScadaBridge.Communication/Actors/CentralCommunicationActor.cs:493` |
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.ConfigurationDatabase` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase` |
|
||||
| Design doc | `docs/requirements/Component-ConfigurationDatabase.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -13,7 +13,7 @@
|
||||
## Summary
|
||||
|
||||
The ConfigurationDatabase module is a focused, conventional EF Core data-access layer:
|
||||
a single `ScadaLinkDbContext`, Fluent API entity configurations, eight repository
|
||||
a single `ScadaBridgeDbContext`, Fluent API entity configurations, eight repository
|
||||
implementations of Commons-defined interfaces, an `IAuditService` implementation, an
|
||||
`IInstanceLocator`, environment-aware migration handling, and design-time tooling
|
||||
support. Overall structure adheres well to the design doc and the CLAUDE.md "Code
|
||||
@@ -151,7 +151,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs:30-41` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs:30-41` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -185,7 +185,7 @@ none consume derived/sub-templates; they all need the template's *member* collec
|
||||
(Attributes/Alarms/Scripts/Compositions), which `GetTemplateByIdAsync` already
|
||||
eager-loads. The `Template` entity has no child-templates navigation collection, and
|
||||
adding one (plus changing the interface signature) would require editing
|
||||
`ScadaLink.Commons`, which is outside this module's scope.
|
||||
`ZB.MOM.WW.ScadaBridge.Commons`, which is outside this module's scope.
|
||||
|
||||
Fix applied the recommendation's secondary option: removed the dead query so the
|
||||
method no longer misleads or wastes a round-trip, and added an XML doc comment
|
||||
@@ -204,12 +204,12 @@ template-aggregate contract the callers depend on.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/DesignTimeDbContextFactory.cs:21-22` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DesignTimeDbContextFactory.cs:21-22` |
|
||||
|
||||
**Description**
|
||||
|
||||
`DesignTimeDbContextFactory` falls back to a literal connection string
|
||||
`"Server=localhost,1433;Database=ScadaLink_Config;User Id=sa;Password=YourPassword;TrustServerCertificate=True"`
|
||||
`"Server=localhost,1433;Database=ScadaBridge_Config;User Id=sa;Password=YourPassword;TrustServerCertificate=True"`
|
||||
when no configured connection string is found. Embedding a credential literal (even a
|
||||
placeholder) in source code is a poor pattern: it is committed to version control,
|
||||
encourages copy-paste of `sa`/`TrustServerCertificate=True` into real environments, and
|
||||
@@ -220,7 +220,7 @@ silently pointing tooling at an unintended database.
|
||||
|
||||
Remove the hardcoded fallback. If no connection string is resolved from configuration
|
||||
or environment, throw a clear `InvalidOperationException` instructing the developer to
|
||||
set `ScadaLink:Database:ConfigurationDb` (or an environment variable). At minimum, read
|
||||
set `ScadaBridge:Database:ConfigurationDb` (or an environment variable). At minimum, read
|
||||
the design-time connection string from an environment variable rather than a literal,
|
||||
and never use `sa`.
|
||||
|
||||
@@ -233,7 +233,7 @@ resolves the connection string from the Host's appsettings files or, when those
|
||||
present, from the `SCADALINK_DESIGNTIME_CONNECTIONSTRING` environment variable, and
|
||||
throws a clear `InvalidOperationException` (naming both the config key and the env var)
|
||||
when neither yields a value. Also hardened `SetBasePath` to be applied only when the
|
||||
`ScadaLink.Host` directory exists, so the factory degrades cleanly instead of throwing
|
||||
`ZB.MOM.WW.ScadaBridge.Host` directory exists, so the factory degrades cleanly instead of throwing
|
||||
`DirectoryNotFoundException` when run from a context without a sibling Host folder.
|
||||
Regression tests added in `DesignTimeDbContextFactoryTests.cs`:
|
||||
`CreateDbContext_NoConnectionStringConfigured_ThrowsClearException`,
|
||||
@@ -247,13 +247,13 @@ Regression tests added in `DesignTimeDbContextFactoryTests.cs`:
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/ServiceCollectionExtensions.cs:44-49` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ServiceCollectionExtensions.cs:44-49` |
|
||||
|
||||
**Description**
|
||||
|
||||
The parameterless `AddConfigurationDatabase()` overload is a deliberate no-op "retained
|
||||
for backward compatibility during migration." If a central node is wired up with this
|
||||
overload by mistake, no `ScadaLinkDbContext`, repositories, `IAuditService`, or
|
||||
overload by mistake, no `ScadaBridgeDbContext`, repositories, `IAuditService`, or
|
||||
`IInstanceLocator` are registered. The failure does not surface at startup; it surfaces
|
||||
much later as opaque DI resolution exceptions the first time any consumer requests a
|
||||
repository — far from the actual misconfiguration. The XML comment also refers to
|
||||
@@ -291,7 +291,7 @@ New regression tests added in `ServiceCollectionExtensionsTests.cs`:
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/NotificationConfiguration.cs:56-57`, `src/ScadaLink.ConfigurationDatabase/Configurations/ExternalSystemConfiguration.cs:25-26,75-77` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/NotificationConfiguration.cs:56-57`, `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/ExternalSystemConfiguration.cs:25-26,75-77` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -325,7 +325,7 @@ backed by ASP.NET Data Protection, which the module already uses
|
||||
(`IDataProtectionKeyContext`, `AddDataProtection().PersistKeysToDbContext`). Added
|
||||
`EncryptedStringConverter` (purpose-scoped `IDataProtector`; `Protect` on write,
|
||||
`Unprotect` on read; null-safe; surfaces a clear message on a `CryptographicException`).
|
||||
`ScadaLinkDbContext` gained an `(options, IDataProtectionProvider)` constructor and
|
||||
`ScadaBridgeDbContext` gained an `(options, IDataProtectionProvider)` constructor and
|
||||
applies the converter to the three secret columns in `OnModelCreating`; the DI
|
||||
registration in `ServiceCollectionExtensions` now constructs the context with the
|
||||
registered provider. The secret columns were widened to `HasMaxLength(8000)` (EF maps
|
||||
@@ -338,7 +338,7 @@ columns plus a null round-trip.
|
||||
The encryption scheme itself is fully in-module; the only remaining cross-cutting item
|
||||
is a documentation gap — the design doc does not yet state encryption-at-rest for these
|
||||
fields. That doc update is outside this module's editable scope (constraint: edit only
|
||||
`src/ScadaLink.ConfigurationDatabase`, the tests, and this file) and is surfaced here
|
||||
`src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase`, the tests, and this file) and is surfaced here
|
||||
for a follow-up to `docs/requirements/Component-ConfigurationDatabase.md`. The audit
|
||||
secret-leak concern is mitigated separately by CD-007's serializer hardening; whether
|
||||
callers should additionally redact secret-bearing entities before passing them to
|
||||
@@ -352,7 +352,7 @@ follow-up. The code fix in this module is complete.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/AuditConfiguration.cs:11` (entity `src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/AuditConfiguration.cs:11` (entity `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Audit/AuditLogEntry.cs`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -374,8 +374,8 @@ Resolve the discrepancy in one direction.
|
||||
|
||||
Resolved 2026-05-16 (commit pending). Root cause confirmed against source: the
|
||||
`AuditLogEntry` entity declares `int Id`, while the design doc's Audit Entry Schema
|
||||
table said `Long / GUID`. The entity lives in `ScadaLink.Commons`
|
||||
(`src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs`), which is outside this
|
||||
table said `Long / GUID`. The entity lives in `ZB.MOM.WW.ScadaBridge.Commons`
|
||||
(`src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Audit/AuditLogEntry.cs`), which is outside this
|
||||
module's editable scope, so the discrepancy was resolved by aligning the design doc to
|
||||
the code — the recommendation's second option. The schema table now records `Id` as
|
||||
`int (identity)` with an explicit justification: a 32-bit identity matches the key type
|
||||
@@ -394,7 +394,7 @@ already exercise the `int` key end to end.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/SiteConfiguration.cs:24-25` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/SiteConfiguration.cs:24-25` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -434,7 +434,7 @@ the bound of the `NodeAAddress`/`NodeBAddress` siblings).
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Services/AuditService.cs:28-30` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/AuditService.cs:28-30` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -483,7 +483,7 @@ added in `AuditServiceTests.cs`:
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/InboundApiRepository.cs:46-58` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/InboundApiRepository.cs:46-58` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -532,7 +532,7 @@ capturing `ILogger` to assert the warning is emitted only on malformed input).
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs:43-51,53-61`, `src/ScadaLink.ConfigurationDatabase/Repositories/CentralUiRepository.cs:45-55` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs:43-51,53-61`, `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/CentralUiRepository.cs:45-55` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -580,7 +580,7 @@ not a 24-row cartesian product) and
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs`, `Repositories/DeploymentManagerRepository.cs`, `Repositories/ExternalSystemRepository.cs`, `Repositories/InboundApiRepository.cs`, `Repositories/NotificationRepository.cs`, `Repositories/SiteRepository.cs`, `Services/InstanceLocator.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs`, `Repositories/DeploymentManagerRepository.cs`, `Repositories/ExternalSystemRepository.cs`, `Repositories/InboundApiRepository.cs`, `Repositories/NotificationRepository.cs`, `Repositories/SiteRepository.cs`, `Services/InstanceLocator.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -629,13 +629,13 @@ as the CD-011 regression guard. The full module suite is green.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/ExternalSystemRepository.cs:11-14`, `Repositories/InboundApiRepository.cs:11-14`, `Repositories/NotificationRepository.cs:11-14`, `Services/InstanceLocator.cs:13-16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/ExternalSystemRepository.cs:11-14`, `Repositories/InboundApiRepository.cs:11-14`, `Repositories/NotificationRepository.cs:11-14`, `Services/InstanceLocator.cs:13-16` |
|
||||
|
||||
**Description**
|
||||
|
||||
`SecurityRepository`, `CentralUiRepository`, `TemplateEngineRepository`,
|
||||
`DeploymentManagerRepository`, `SiteRepository`, and `AuditService` all guard their
|
||||
injected `ScadaLinkDbContext` with `?? throw new ArgumentNullException(...)`.
|
||||
injected `ScadaBridgeDbContext` with `?? throw new ArgumentNullException(...)`.
|
||||
`ExternalSystemRepository`, `InboundApiRepository`, `NotificationRepository`, and
|
||||
`InstanceLocator` assign the constructor argument directly with no guard. This is a
|
||||
minor consistency/maintainability issue: although the DI container will not normally
|
||||
@@ -651,7 +651,7 @@ inconsistent constructors so all data-access types behave uniformly.
|
||||
|
||||
Resolved 2026-05-16 (commit pending). Root cause confirmed against source:
|
||||
`ExternalSystemRepository`, `InboundApiRepository`, `NotificationRepository`, and
|
||||
`InstanceLocator` assigned the injected `ScadaLinkDbContext` directly with no null
|
||||
`InstanceLocator` assigned the injected `ScadaBridgeDbContext` directly with no null
|
||||
guard, diverging from `SecurityRepository`/`CentralUiRepository`/`TemplateEngineRepository`/
|
||||
`DeploymentManagerRepository`/`SiteRepository`/`AuditService`. Applied the recommendation:
|
||||
all four constructors now use `context ?? throw new ArgumentNullException(nameof(context))`
|
||||
@@ -668,7 +668,7 @@ Regression: `Constructor_NullContext_Throws` tests were added for all four affec
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/InboundApiConfiguration.cs:17-19` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/InboundApiConfiguration.cs:17-19` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -720,10 +720,10 @@ Design implemented — **deterministic keyed hash** (the recommendation's first
|
||||
is already a high-entropy random token, and a random salt would break the
|
||||
deterministic by-value lookup the authentication path relies on. The pepper instead
|
||||
binds every hash to the deployment. Implemented as `IApiKeyHasher` / `ApiKeyHasher`
|
||||
in `ScadaLink.Commons` (`Types/InboundApi/ApiKeyHasher.cs`); the constructor
|
||||
in `ZB.MOM.WW.ScadaBridge.Commons` (`Types/InboundApi/ApiKeyHasher.cs`); the constructor
|
||||
rejects a missing or weak (`< 16`-char) pepper with `ArgumentException` — fail-fast.
|
||||
- **Where the pepper lives.** `InboundApiOptions.ApiKeyPepper`, a component-owned
|
||||
Options class already bound from the `ScadaLink:InboundApi` configuration section
|
||||
Options class already bound from the `ScadaBridge:InboundApi` configuration section
|
||||
(Options pattern); it is never hard-coded. `AddInboundAPI` registers `IApiKeyHasher`
|
||||
via a factory that reads the bound options, so a missing/weak pepper fails the
|
||||
deployment fast rather than degrading silently. (Operators must supply the pepper
|
||||
@@ -772,14 +772,14 @@ doc, and update the Central UI API-keys page, which previously displayed a maske
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/ScadaLinkDbContext.cs:107-124` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ScadaBridgeDbContext.cs:107-124` |
|
||||
|
||||
**Description**
|
||||
|
||||
`ApplySecretColumnEncryption` resolves the Data Protection provider as
|
||||
`_dataProtectionProvider ?? new EphemeralDataProtectionProvider()`. The `??` fallback
|
||||
is reached whenever the context is constructed via the single-argument
|
||||
`ScadaLinkDbContext(DbContextOptions)` constructor — i.e. whenever no provider was
|
||||
`ScadaBridgeDbContext(DbContextOptions)` constructor — i.e. whenever no provider was
|
||||
injected. An `EphemeralDataProtectionProvider` generates a key ring that lives only in
|
||||
process memory and is discarded at process exit.
|
||||
|
||||
@@ -788,8 +788,8 @@ it only emits schema). The risk is on a *runtime write path*. The runtime curren
|
||||
gets the provider-bearing context only because `AddConfigurationDatabase` adds an
|
||||
`AddScoped` factory registration that overrides EF's activator-based registration.
|
||||
That override is the single thing standing between correct behaviour and silent data
|
||||
corruption: any future change that resolves a `ScadaLinkDbContext` through a path the
|
||||
override does not cover — an `AddPooledDbContextFactory`/`IDbContextFactory<ScadaLinkDbContext>`
|
||||
corruption: any future change that resolves a `ScadaBridgeDbContext` through a path the
|
||||
override does not cover — an `AddPooledDbContextFactory`/`IDbContextFactory<ScadaBridgeDbContext>`
|
||||
registration, a second `AddDbContext` call, a hand-constructed context in server code —
|
||||
would construct the context with the single-arg constructor, encrypt secret columns
|
||||
with a throwaway key, and persist ciphertext that becomes **permanently undecryptable
|
||||
@@ -806,7 +806,7 @@ single-arg constructor but mark contexts built without a real provider as
|
||||
schema-only — e.g. record a flag and have the encrypting converter throw a clear
|
||||
`InvalidOperationException` ("secret columns cannot be written without a configured
|
||||
Data Protection key ring") on the first `Protect`, instead of producing throwaway
|
||||
ciphertext. Also harden the DI wiring so a `ScadaLinkDbContext` cannot be resolved
|
||||
ciphertext. Also harden the DI wiring so a `ScadaBridgeDbContext` cannot be resolved
|
||||
through the EF-activator registration at all (e.g. register only the factory, or use
|
||||
`AddDbContextFactory` with the explicit constructor).
|
||||
|
||||
@@ -847,7 +847,7 @@ fail-fast guard now closes the residual gap for any other resolution path.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/ScadaLinkDbContext.cs:121-123` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ScadaBridgeDbContext.cs:121-123` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -892,7 +892,7 @@ columns) — asserting each column keeps an `EncryptedStringConverter`.
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs:33-45` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs:33-45` |
|
||||
|
||||
**Resolution** — rewrote `InsertIfNotExistsAsync` as a single raw-SQL
|
||||
`IF NOT EXISTS (...) INSERT` matching the
|
||||
@@ -903,7 +903,7 @@ catch on numbers 2601 (unique-index violation) and 2627
|
||||
losers are logged at Debug and treated as no-ops, eliminating the
|
||||
site-retry livelock. Two SQLite-targeted assertions in
|
||||
`RepositoryCoverageTests` were migrated to a new MS SQL-fixture file
|
||||
`tests/ScadaLink.ConfigurationDatabase.Tests/Repositories/NotificationOutboxRepositoryIntegrationTests.cs`,
|
||||
`tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Repositories/NotificationOutboxRepositoryIntegrationTests.cs`,
|
||||
which also adds a 50-way parallel race test verifying exactly one row
|
||||
lands and no exception bubbles.
|
||||
|
||||
@@ -947,7 +947,7 @@ throws and exactly one row lands.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/InboundApiRepository.cs:35-39` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/InboundApiRepository.cs:35-39` |
|
||||
|
||||
**Resolution (2026-05-28):** Took option (a) — `InboundApiRepository` ctor now
|
||||
accepts `Func<IApiKeyHasher>? hasherAccessor = null` (deferred resolution to
|
||||
@@ -986,7 +986,7 @@ as "key not found".
|
||||
**Recommendation**
|
||||
|
||||
Either (a) take `IApiKeyHasher` via constructor injection — alongside the existing
|
||||
`ScadaLinkDbContext` and optional `ILogger` — and use it here so the repository
|
||||
`ScadaBridgeDbContext` and optional `ILogger` — and use it here so the repository
|
||||
participates in the same peppered scheme as the rest of the system; or (b) delete
|
||||
the method from both the implementation and `IInboundApiRepository` (Commons) on the
|
||||
grounds that the production authentication path correctly avoids it for timing
|
||||
@@ -1003,7 +1003,7 @@ longer exists.
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs:83-97` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs:83-97` |
|
||||
|
||||
**Resolution (2026-05-28):**
|
||||
`IDeploymentManagerRepository.DeleteDeploymentRecordAsync` now requires a `byte[] expectedRowVersion`
|
||||
@@ -1062,9 +1062,9 @@ when the real RowVersion is supplied.
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs`, `Configurations/SiteCallEntityTypeConfiguration.cs` (mappings for `OccurredAtUtc`, `IngestedAtUtc`, `CreatedAtUtc`, `UpdatedAtUtc`, `TerminalAtUtc`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs`, `Configurations/SiteCallEntityTypeConfiguration.cs` (mappings for `OccurredAtUtc`, `IngestedAtUtc`, `CreatedAtUtc`, `UpdatedAtUtc`, `TerminalAtUtc`) |
|
||||
|
||||
**Resolution (2026-05-28):** Added two private static `ValueConverter<DateTime, DateTime>` / `ValueConverter<DateTime?, DateTime?>` UTC-enforcing converters to `AuditLogEntityTypeConfiguration` and applied them to `AuditEvent.OccurredAtUtc` and `AuditEvent.IngestedAtUtc` via `HasConversion(...)`. The converter re-tags `DateTimeKind.Utc` on hydrate (where SQL Server's `datetime2` provider strips the Kind flag) and on write (so a producer-supplied `Kind=Unspecified` literal still lands as UTC in the model cache). Coordinates with the sibling `Commons-019` resolution (init-setter on `AuditEvent` re-tags Kind=Utc at construction). Regression test in `tests/ScadaLink.ConfigurationDatabase.Tests/Configurations/AuditLogEntityTypeConfigurationTests.cs::Configure_UtcConverter_HydratesOccurredAtUtcAsKindUtc` inserts an Unspecified-Kind value, re-reads through a cleared change-tracker, and asserts `Kind == Utc` on both columns. The `SiteCall` mapping is out of scope for this close (sibling component task).
|
||||
**Resolution (2026-05-28):** Added two private static `ValueConverter<DateTime, DateTime>` / `ValueConverter<DateTime?, DateTime?>` UTC-enforcing converters to `AuditLogEntityTypeConfiguration` and applied them to `AuditEvent.OccurredAtUtc` and `AuditEvent.IngestedAtUtc` via `HasConversion(...)`. The converter re-tags `DateTimeKind.Utc` on hydrate (where SQL Server's `datetime2` provider strips the Kind flag) and on write (so a producer-supplied `Kind=Unspecified` literal still lands as UTC in the model cache). Coordinates with the sibling `Commons-019` resolution (init-setter on `AuditEvent` re-tags Kind=Utc at construction). Regression test in `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Configurations/AuditLogEntityTypeConfigurationTests.cs::Configure_UtcConverter_HydratesOccurredAtUtcAsKindUtc` inserts an Unspecified-Kind value, re-reads through a cleared change-tracker, and asserts `Kind == Utc` on both columns. The `SiteCall` mapping is out of scope for this close (sibling component task).
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1115,7 +1115,7 @@ modules.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Maintenance/AuditLogPartitionMaintenance.cs:181-199` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Maintenance/AuditLogPartitionMaintenance.cs:181-199` |
|
||||
|
||||
**Resolution (2026-05-28):** Took option (a) — dropped the `try/catch (SqlException)`
|
||||
around the per-month SPLIT loop entirely (and the now-unused
|
||||
@@ -1142,7 +1142,7 @@ by either path."
|
||||
|
||||
That rationale is correct only for an "already-exists" error — which the pre-check
|
||||
makes impossible. Any *other* `SqlException` — a permissions failure (the
|
||||
`scadalink_audit_purger` role's `ALTER ON SCHEMA::dbo` revoked or not granted), a
|
||||
`scadabridge_audit_purger` role's `ALTER ON SCHEMA::dbo` revoked or not granted), a
|
||||
deadlock victim, a transient connection drop, a transaction log full, an underlying
|
||||
filegroup full — leaves the boundary genuinely **not** created, logs a Warning
|
||||
(quiet by default in most appenders), and the next iteration tries to SPLIT the
|
||||
@@ -1174,7 +1174,7 @@ aborts after the first failure with no further SPLITs.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/AuditLogRepository.cs:378-387` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/AuditLogRepository.cs:378-387` |
|
||||
|
||||
**Resolution (2026-05-28):** Wrapped the `reader.GetDateTime(0)` read with `DateTime.SpecifyKind(..., DateTimeKind.Utc)` so each returned boundary now carries `Kind=Utc`, matching the explicit defensive pattern already in `AuditLogPartitionMaintenance.GetMaxBoundaryAsync`. Added an inline comment explaining the rationale (SQL Server `datetime2` strips Kind through ADO.NET; boundary values are stored UTC). With sibling CD-018 also closed, the EF read path now enforces UTC at the column level — the raw-ADO defence here is belt-and-braces for this method, which bypasses EF entirely.
|
||||
|
||||
@@ -1205,7 +1205,7 @@ converter on the column) so the defence at the read site is no longer required.
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/AuditLogRepository.cs:192-338` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/AuditLogRepository.cs:192-338` |
|
||||
|
||||
**Resolution (2026-05-28):** Took the targeted (1) part of the recommendation —
|
||||
the `monthBoundary` format string is now `"yyyy-MM-dd HH:mm:ss.fffffff"`
|
||||
@@ -1263,7 +1263,7 @@ boundary lookup resolves to the expected partition.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs:8-14` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs:8-14` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1301,7 +1301,7 @@ No behaviour change.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs:99-101`, `Migrations/20260520142214_AddAuditLogTable.cs:103-107` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs:99-101`, `Migrations/20260520142214_AddAuditLogTable.cs:103-107` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1341,7 +1341,7 @@ index name remains `IX_AuditLog_CorrelationId`.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.ConfigurationDatabase.Tests/Maintenance/AuditLogPartitionMaintenanceTests.cs`, `tests/.../RepositoryCoverageTests.cs:855-869` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Maintenance/AuditLogPartitionMaintenanceTests.cs`, `tests/.../RepositoryCoverageTests.cs:855-869` |
|
||||
|
||||
**Resolution (2026-05-28):** (1) Added `AuditLogPartitionMaintenanceTests.EnsureLookahead_SecondSplitThrows_LoopAborts_FirstBoundaryStillCommitted` (Skippable, MS SQL fixture) — installs a `DbCommandInterceptor` that lets the 1st `ALTER PARTITION FUNCTION pf_AuditLog_Month() SPLIT RANGE` through and throws on the 2nd, asserts the exception propagates (CD-019's no-try/catch behaviour), counts exactly one successful split, and verifies the first boundary IS now persisted in `pf_AuditLog_Month` so the next tick resumes from N+1 with no holes. (2) Added `DeploymentManagerRepositoryTests.DeleteDeploymentRecord_CurrentRowVersion_StubAttachPath_DeleteSucceeds` — production-shape happy path: caller holds the current RowVersion, change-tracker cleared, delete completes without throwing `DbUpdateConcurrencyException` and the row is gone (1 row affected).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.DataConnectionLayer` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer` |
|
||||
| Design doc | `docs/requirements/Component-DataConnectionLayer.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -123,7 +123,7 @@ DCL-007 fixed for `ReadBatchAsync`). New findings are numbered from
|
||||
| Severity | Critical |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:473-538` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:473-538` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -171,7 +171,7 @@ whose message references `DataConnectionLayer-001`.
|
||||
| Severity | High |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionManagerActor.cs:131-141` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionManagerActor.cs:131-141` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -204,7 +204,7 @@ intact across a transient handler exception, so the design doc's "transparent
|
||||
re-subscribe" guarantee (WP-10) is preserved. The actor is a long-lived stateful
|
||||
coordinator and its own Become/Stash reconnect state machine already recovers
|
||||
connection-level faults — it does not need a restart. This also aligns with the
|
||||
ScadaLink convention of `Resume` for coordinator actors. Regression test
|
||||
ScadaBridge convention of `Resume` for coordinator actors. Regression test
|
||||
`DCL002_ConnectionActorCrash_PreservesSubscriptionState` crashes the connection actor
|
||||
via a synchronously-throwing write and asserts the subscription survives (health
|
||||
report still shows 1 subscribed/resolved tag); it fails against the pre-fix `Restart`
|
||||
@@ -218,7 +218,7 @@ code and passes after. Fixed by the commit whose message references
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Adapters/RealOpcUaClient.cs:16-17,130-131,153,163,173,183-184` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs:16-17,130-131,153,163,173,183-184` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -264,7 +264,7 @@ code and passes after. Fixed by the commit whose message references
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:495-503,529-537` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:495-503,529-537` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -313,7 +313,7 @@ against the pre-fix code and pass after. Fixed by the commit whose message refer
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/DataConnectionOptions.cs:15`, `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:573-590` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionOptions.cs:15`, `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:573-590` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -357,7 +357,7 @@ unbounded code and passes after. Fixed by the commit whose message references
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:645-673,721-756` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:645-673,721-756` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -408,7 +408,7 @@ code and 1 after.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:187-195` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:187-195` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -451,7 +451,7 @@ after.
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:540-569` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:540-569` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -501,7 +501,7 @@ refactor.)
|
||||
| Severity | Medium — partially design-doc work outside this module's editable scope |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:189,242-297,379-449`, `docs/requirements/Component-DataConnectionLayer.md:73-85` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:189,242-297,379-449`, `docs/requirements/Component-DataConnectionLayer.md:73-85` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -541,7 +541,7 @@ describes only the connect-failure failover path and does not mention the
|
||||
unstable-disconnect trigger. **Action required (surfaced):** the DCL design doc should
|
||||
be updated to document the unstable-disconnect failover path and the configurable
|
||||
stability threshold; that edit was deliberately not made here because this task is
|
||||
scoped to `src/ScadaLink.DataConnectionLayer`, tests, and this findings file only.
|
||||
scoped to `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer`, tests, and this findings file only.
|
||||
|
||||
### DataConnectionLayer-010 — Tag-resolution retry can issue duplicate concurrent subscribe attempts
|
||||
|
||||
@@ -550,7 +550,7 @@ scoped to `src/ScadaLink.DataConnectionLayer`, tests, and this findings file onl
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:594-619,689-703` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:594-619,689-703` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -596,7 +596,7 @@ subscribe calls); the pre-fix code dispatched on every tick (6 total).
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:486-489,278-285,416-425`, `src/ScadaLink.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:252-262` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:486-489,278-285,416-425`, `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:252-262` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -644,7 +644,7 @@ subscriber against the pre-fix code and is dropped after.
|
||||
| Severity | Medium — full secure default also requires a Commons + design-doc change outside this module |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Adapters/IOpcUaClient.cs:17`, `src/ScadaLink.DataConnectionLayer/Adapters/RealOpcUaClient.cs:49,60-61`, `docs/requirements/Component-DataConnectionLayer.md:116` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/IOpcUaClient.cs:17`, `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs:49,60-61`, `docs/requirements/Component-DataConnectionLayer.md:116` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -660,7 +660,7 @@ untrusted certs unless an operator opts in per connection.
|
||||
**Verification note**: Confirmed against source. Note the *authoritative* runtime
|
||||
default does not actually live on `OpcUaConnectionOptions` — for a real connection
|
||||
`OpcUaDataConnection.ConnectAsync` builds `OpcUaConnectionOptions` from
|
||||
`OpcUaEndpointConfig` (in `ScadaLink.Commons`), whose `AutoAcceptUntrustedCerts`
|
||||
`OpcUaEndpointConfig` (in `ZB.MOM.WW.ScadaBridge.Commons`), whose `AutoAcceptUntrustedCerts`
|
||||
property also defaults to `true`. `OpcUaConnectionOptions`' own default is only the
|
||||
fallback used when an `OpcUaConnectionOptions` is constructed directly.
|
||||
|
||||
@@ -680,13 +680,13 @@ was added as an optional constructor parameter, defaulting to `NullLogger`, so
|
||||
existing callers are unaffected). Regression test
|
||||
`DCL012_OpcUaConnectionOptions_AutoAcceptUntrustedCerts_DefaultsToFalse` guards the
|
||||
new secure default. **Two parts remain outside this module's editable scope and are
|
||||
surfaced as action required:** (a) `ScadaLink.Commons.Types.DataConnections.OpcUaEndpointConfig.AutoAcceptUntrustedCerts`
|
||||
surfaced as action required:** (a) `ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections.OpcUaEndpointConfig.AutoAcceptUntrustedCerts`
|
||||
still defaults to `true` — since that is the value actually used for a real connection
|
||||
(see verification note above), the Commons default must also be flipped to `false`
|
||||
for the system to be secure-by-default; (b) `docs/requirements/Component-DataConnectionLayer.md`
|
||||
line 116 still documents `true` as the default and must be updated. Both edits were
|
||||
deliberately not made here because this task is scoped to
|
||||
`src/ScadaLink.DataConnectionLayer`, tests, and this findings file only.
|
||||
`src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer`, tests, and this findings file only.
|
||||
|
||||
### DataConnectionLayer-013 — Misleading XML comment: `RaiseDisconnected` claims thread safety it does not provide
|
||||
|
||||
@@ -695,7 +695,7 @@ deliberately not made here because this task is scoped to
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:270-281` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:270-281` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -738,7 +738,7 @@ guard), and it passes against the atomic fix.
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Adapters/RealOpcUaClient.cs:325`, `src/ScadaLink.DataConnectionLayer/Adapters/RealOpcUaClient.cs:35-39,79-83` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs:325`, `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs:35-39,79-83` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -793,7 +793,7 @@ and pass after.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:404-417`, `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:419-493` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:404-417`, `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:419-493` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -847,7 +847,7 @@ guards the no-backup path.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:606,666-672`, `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:232-240` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:606,666-672`, `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:232-240` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -901,7 +901,7 @@ the pre-fix code (it returned `Success: true`) and passes after;
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:229-237`, `src/ScadaLink.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:218-227` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:229-237`, `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:218-227` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -953,7 +953,7 @@ pre-fix code (the batch throws, no map returned) and passes after;
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:557,564-594,653` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:557,564-594,653` |
|
||||
|
||||
**Resolution** — added a `_subscribesInFlight` HashSet mirroring the
|
||||
existing `_resolutionInFlight` pattern. `HandleSubscribe` now partitions
|
||||
@@ -1018,7 +1018,7 @@ exactly one `_adapter.SubscribeAsync(tag, ...)` call (and no orphan subscription
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:31,167,177`, `src/ScadaLink.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:163-164` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:31,167,177`, `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaDataConnection.cs:163-164` |
|
||||
|
||||
**Resolution** — deleted the dead `_subscriptionHandles` field outright.
|
||||
Subscription bookkeeping lives in `RealOpcUaClient._monitoredItems` /
|
||||
@@ -1085,7 +1085,7 @@ state.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:653-661,670-688` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:653-661,670-688` |
|
||||
|
||||
**Resolution** — split the success branch into "fresh subscribe" vs
|
||||
"unresolved → resolved promotion": `_unresolvedTags.Remove(...)` is now
|
||||
@@ -1150,7 +1150,7 @@ test that asserts `_totalSubscribed` / `_resolvedTags` consistency after the
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:626-634,642-687` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:626-634,642-687` |
|
||||
|
||||
**Resolution** — `HandleSubscribeCompleted` now detects the
|
||||
mid-termination race: when `_subscriptionsByInstance.TryGetValue` fails,
|
||||
@@ -1218,7 +1218,7 @@ for A while the subscribe I/O is in flight, completes the subscribe, and asserts
|
||||
| Severity | Medium |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs:691-698,991-998` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs:691-698,991-998` |
|
||||
|
||||
**Resolution** — both call sites now gate
|
||||
`Timers.StartPeriodicTimer("tag-resolution-retry", ...)` with
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.DeploymentManager` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager` |
|
||||
| Design doc | `docs/requirements/Component-DeploymentManager.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -109,7 +109,7 @@ The remaining six findings are medium/low: lifecycle-timeout audit gap
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:141-199` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:141-199` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -151,7 +151,7 @@ stuck in `InProgress`. Regression test:
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:186-196` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:186-196` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -190,7 +190,7 @@ error) if persistence still fails. Regression test:
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:155-170` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:155-170` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -237,7 +237,7 @@ Regression test:
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:312-319` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:312-319` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -278,7 +278,7 @@ record is orphaned and must be reconciled. Regression test:
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/OperationLockManager.cs:15-33` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/OperationLockManager.cs:15-33` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -323,7 +323,7 @@ Regression tests: `AcquireAsync_ReleasedLock_RemovesSemaphoreEntry`,
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:84-200,363-368` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:84-200,363-368` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -386,7 +386,7 @@ stale-rejection) when the query fails. Regression tests:
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:334-358,401-406` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:334-358,401-406` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -432,7 +432,7 @@ Regression test: `GetDeploymentComparisonAsync_ProducesStructuredDiff`.
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/ServiceCollectionExtensions.cs:7-14` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ServiceCollectionExtensions.cs:7-14` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -453,14 +453,14 @@ no equivalent for `DeploymentManagerOptions`.
|
||||
|
||||
Add an `IConfiguration` parameter (or a configure callback) to
|
||||
`AddDeploymentManager` and bind `DeploymentManagerOptions` to a section such as
|
||||
`ScadaLink:DeploymentManager`, consistent with the other components.
|
||||
`ScadaBridge:DeploymentManager`, consistent with the other components.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-16 (commit pending): `AddDeploymentManager()` now calls
|
||||
`services.AddOptions<DeploymentManagerOptions>()` so `IOptions<DeploymentManagerOptions>`
|
||||
is always resolvable, and `Host/Program.cs` binds the
|
||||
`ScadaLink:DeploymentManager` section (exposed as
|
||||
`ScadaBridge:DeploymentManager` section (exposed as
|
||||
`ServiceCollectionExtensions.OptionsSection`) via
|
||||
`services.Configure<DeploymentManagerOptions>(...)` — the same pattern the Host
|
||||
uses for `SecurityOptions`/`InboundApiOptions`. An earlier attempt added an
|
||||
@@ -479,7 +479,7 @@ configuration binding. Regression tests:
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:288` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:288` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -515,7 +515,7 @@ would be meaningless).
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/ArtifactDeploymentService.cs:136,194-211` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs:136,194-211` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -546,8 +546,8 @@ the audit log, and the UI summary all reference a single id instead of N+1
|
||||
unrelated GUIDs (`RetryForSiteAsync`, an independent single-site retry, still
|
||||
mints its own id). Adding a dedicated `DeploymentId` *column* to
|
||||
`SystemArtifactDeploymentRecord` was deliberately **not** done: that entity
|
||||
lives in `ScadaLink.Commons` with its EF mapping in
|
||||
`ScadaLink.ConfigurationDatabase`, both outside this module's edit scope.
|
||||
lives in `ZB.MOM.WW.ScadaBridge.Commons` with its EF mapping in
|
||||
`ZB.MOM.WW.ScadaBridge.ConfigurationDatabase`, both outside this module's edit scope.
|
||||
Instead the logical `deploymentId` is embedded in the record's free-form
|
||||
`PerSiteStatus` JSON payload (`{ DeploymentId, Sites }`), which is fully within
|
||||
this module's control, so the persisted record is correlatable with the
|
||||
@@ -564,7 +564,7 @@ Regression tests: `DeployToAllSitesAsync_AllPerSiteCommandsShareTheSummaryDeploy
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.DeploymentManager.Tests/DeploymentServiceTests.cs:100-151,155-199` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/DeploymentServiceTests.cs:100-151,155-199` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -619,7 +619,7 @@ which asserts on `IAuditService.LogAsync`. Regression tests:
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentManagerOptions.cs:8-9` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentManagerOptions.cs:8-9` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -664,7 +664,7 @@ the call hung the full 30 s and threw `AskTimeoutException`).
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/ArtifactDeploymentService.cs:108-111` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs:108-111` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -690,14 +690,14 @@ into the artifact (which the design explicitly mandates — SMTP configuration i
|
||||
a deployable artifact) and **never logs it** — the three log statements in
|
||||
`DeployToAllSitesAsync` only reference `SiteId`, `SiteName`, `DeploymentId`, and
|
||||
`ex.Message`, never the credential. There is no defect to fix purely within
|
||||
`src/ScadaLink.DeploymentManager`. The finding's remaining recommendations are
|
||||
`src/ZB.MOM.WW.ScadaBridge.DeploymentManager`. The finding's remaining recommendations are
|
||||
all cross-module and one needs a design decision:
|
||||
- inter-cluster transport TLS — `ScadaLink.Communication` /
|
||||
`ScadaLink.ClusterInfrastructure` (Akka remoting + ClusterClient config);
|
||||
- at-rest encryption of the credential on site SQLite — `ScadaLink.SiteRuntime`
|
||||
- inter-cluster transport TLS — `ZB.MOM.WW.ScadaBridge.Communication` /
|
||||
`ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` (Akka remoting + ClusterClient config);
|
||||
- at-rest encryption of the credential on site SQLite — `ZB.MOM.WW.ScadaBridge.SiteRuntime`
|
||||
artifact store;
|
||||
- encrypting the credential field inside the artifact payload — needs the
|
||||
`SmtpConfigurationArtifact` shape in `ScadaLink.Commons` plus cooperating
|
||||
`SmtpConfigurationArtifact` shape in `ZB.MOM.WW.ScadaBridge.Commons` plus cooperating
|
||||
producer (DeploymentManager) and consumer (SiteRuntime) changes, and a
|
||||
**key-management design decision** (where the encryption key lives, how it
|
||||
is distributed to sites) that cannot be made unilaterally here.
|
||||
@@ -733,7 +733,7 @@ option rather than an open defect.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.DeploymentManager.Tests/ArtifactDeploymentServiceTests.cs:86-90` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/ArtifactDeploymentServiceTests.cs:86-90` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -773,7 +773,7 @@ covers DeploymentManager-010), `DeployToAllSitesAsync_PartialFailure_ReportsPerS
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:631-655` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:631-655` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -834,7 +834,7 @@ threaded into `TryReconcileWithSiteAsync`). Regression test:
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:639-651` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:639-651` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -877,7 +877,7 @@ entry, and the site's actually-applied revision all agree. Regression test:
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:562-570` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:562-570` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -913,7 +913,7 @@ would be meaningless).
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:675-682,721-748` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:675-682,721-748` |
|
||||
|
||||
**Resolution** — Added a `forceEnabledState` parameter to `ApplyPostSuccessSideEffectsAsync`. The normal deploy path passes `true` (fresh apply legitimately ends in `Enabled`); the reconciliation path passes `false`, so the helper only promotes `NotDeployed → Enabled` and leaves an existing `Disabled` (or `Enabled`) untouched. Regression test `DeployInstanceAsync_Reconciled_DisabledInstance_PreservesDisabledState` exercises the failover scenario and asserts the prior record still flips to `Success` while `Instance.State` stays `Disabled`.
|
||||
|
||||
@@ -974,7 +974,7 @@ be marked `Success`.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:328-339,385-396,445-458` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:328-339,385-396,445-458` |
|
||||
|
||||
**Resolution (2026-05-28):** added `TryLogLifecycleTimeoutAsync`, a private
|
||||
helper that mirrors the `DeployFailed` pattern — it calls `_auditService.LogAsync`
|
||||
@@ -1032,7 +1032,7 @@ also produces an audit entry.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:698-712` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:698-712` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1077,7 +1077,7 @@ as the actor. Tests green (80/80 in DeploymentManager.Tests).
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:107-111` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:107-111` |
|
||||
|
||||
**Resolution (2026-05-28):** `ResolveSiteIdentifierAsync` now throws `InvalidOperationException` (`"Site with ID {siteId} not found; cannot resolve its SiteIdentifier for routing."`) when the `Site` row is missing, instead of returning the numeric id rendered as a string. The deploy path's existing try/catch turns the throw into a `DeploymentStatus.Failed` record carrying the descriptive message (the `DeploymentManager-001`/`-002` cleanup write the failure with `CancellationToken.None`); the lifecycle paths (Disable/Enable/Delete) propagate the exception so the CLI/UI caller surfaces the actual cause to the operator rather than seeing a confusing downstream "unknown site" routing error. The repository contract already returned `Site?`, so the null path is now type-visible at the call site instead of silently papered over.
|
||||
|
||||
@@ -1117,7 +1117,7 @@ returns `Site?`, so the null path is type-visible; just don't paper over it.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/DeploymentService.cs:178-194` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs:178-194` |
|
||||
|
||||
**Resolution (2026-05-28):** The transient `Pending` write was dropped — the deployment record is now created directly in `DeploymentStatus.InProgress`, which collapses the start of the deploy into a single `AddDeploymentRecordAsync` + `SaveChangesAsync` + `NotifyStatusChange` (instead of two writes back-to-back). The flattening, validation, and `TryReconcileWithSiteAsync` round-trip have all completed before the insert, and the deploy command is sent immediately after, so `Pending` carried no operational meaning between the two writes. `InProgress` retains its documented "sent to site, awaiting response" semantics. Eliminating the extra `SaveChangesAsync` round-trip also removes the `Pending`→`InProgress` flicker the CentralUI-006 deployment-status page used to render via the second `IDeploymentStatusNotifier.NotifyStatusChanged` invocation.
|
||||
|
||||
@@ -1162,7 +1162,7 @@ Either:
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.DeploymentManager/ArtifactDeploymentService.cs:82-144,169-173` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs:82-144,169-173` |
|
||||
|
||||
**Resolution (2026-05-28):** Hoisted the global artifact queries (shared scripts, external systems + methods, DB connections, notification lists, SMTP configurations) out of the per-site loop into a new private `FetchGlobalArtifactsAsync` that produces a `GlobalArtifactSnapshot` record. `DeployToAllSitesAsync` now calls it ONCE before the loop and threads the snapshot through a new prefetched-globals overload of `BuildDeployArtifactsCommandAsync`; the public single-site overload keeps the prior fetch-then-build behaviour for `RetryForSiteAsync`. Only the per-site data-connection query remains inside the loop. Regression tests `DeployToAllSitesAsync_HoistsGlobalArtifactQueriesOutOfPerSiteLoop` (three sites; pins exactly-one call to each global getter and one per-site call to `GetDataConnectionsBySiteIdAsync`) and `RetryForSiteAsync_SingleSitePath_StillRunsTheGlobalQueriesOnce` (single-site path still owns its own fetch).
|
||||
|
||||
@@ -1207,9 +1207,9 @@ N-site deployment.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.DeploymentManager.Tests/DeploymentServiceTests.cs:966-1075`, `tests/ScadaLink.DeploymentManager.Tests/ArtifactDeploymentServiceTests.cs:196-217` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/DeploymentServiceTests.cs:966-1075`, `tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/ArtifactDeploymentServiceTests.cs:196-217` |
|
||||
|
||||
**Resolution (2026-05-28):** Replaced the `static` counters with per-test instance state. Introduced `ReconcileProbeCounters` and `SerializationProbeCounters` (in `DeploymentServiceTests`) and `ArtifactProbeRecorder` (in `ArtifactDeploymentServiceTests`); each probe actor now takes the counter object as its first constructor argument. Every test instantiates a fresh counter local, passes it via `Props.Create(() => new ReconcileProbeActor(counters, ...))`, and reads the counts directly off `counters` — no shared static fields remain. `ReconcileProbeActor`'s counter increments swap to `Interlocked.Increment` for the cross-thread CAS, and `SerializationProbeActor` retains its lock on a per-test `Gate`. All 85 `ScadaLink.DeploymentManager.Tests` continue to pass after the refactor.
|
||||
**Resolution (2026-05-28):** Replaced the `static` counters with per-test instance state. Introduced `ReconcileProbeCounters` and `SerializationProbeCounters` (in `DeploymentServiceTests`) and `ArtifactProbeRecorder` (in `ArtifactDeploymentServiceTests`); each probe actor now takes the counter object as its first constructor argument. Every test instantiates a fresh counter local, passes it via `Props.Create(() => new ReconcileProbeActor(counters, ...))`, and reads the counts directly off `counters` — no shared static fields remain. `ReconcileProbeActor`'s counter increments swap to `Interlocked.Increment` for the cross-thread CAS, and `SerializationProbeActor` retains its lock on a per-test `Gate`. All 85 `ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests` continue to pass after the refactor.
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.ExternalSystemGateway` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway` |
|
||||
| Design doc | `docs/requirements/Component-ExternalSystemGateway.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -120,7 +120,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | Critical |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:109`, `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:81` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:109`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/DatabaseGateway.cs:81` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -176,7 +176,7 @@ transient-retry paths. Fixed by the commit whose message references
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:130`, `src/ScadaLink.ExternalSystemGateway/ServiceCollectionExtensions.cs:13` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:130`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ServiceCollectionExtensions.cs:13` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -221,7 +221,7 @@ swallowed as transient. Regression tests:
|
||||
`Call_CallerCancellation_IsNotMisreportedAsTimeout`.
|
||||
|
||||
Note (partial scope): the per-*system* `Timeout` field on `ExternalSystemDefinition`
|
||||
remains unimplemented — adding it requires a change to `ScadaLink.Commons`, which is
|
||||
remains unimplemented — adding it requires a change to `ZB.MOM.WW.ScadaBridge.Commons`, which is
|
||||
outside this module's edit scope. Until that entity field exists, the configured
|
||||
`DefaultHttpTimeout` is the effective per-call limit for every system. A follow-up
|
||||
against the Commons module should add the `Timeout` field and have `InvokeHttpAsync`
|
||||
@@ -234,7 +234,7 @@ prefer it over the default. This is a tracked follow-up, not a regression.
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:84-117` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:84-117` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -282,7 +282,7 @@ is flipped back to `true`.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:114-115`, `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:86-87` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:114-115`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/DatabaseGateway.cs:86-87` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -291,7 +291,7 @@ is flipped back to `true`.
|
||||
(`MaxRetries > 0 ? ... : null`, `RetryDelay > TimeSpan.Zero ? ... : null`), otherwise
|
||||
falling back to the S&F defaults. The site-side repository that supplies these
|
||||
definitions, `SiteExternalSystemRepository.MapExternalSystem`
|
||||
(`src/ScadaLink.SiteRuntime/Repositories/SiteExternalSystemRepository.cs:194`), never
|
||||
(`src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs:194`), never
|
||||
reads `MaxRetries`/`RetryDelay` from SQLite at all — the constructed entities always
|
||||
have `MaxRetries == 0` and `RetryDelay == TimeSpan.Zero`. As a result, at sites the
|
||||
per-system retry settings the design doc requires are *always* discarded and the
|
||||
@@ -331,7 +331,7 @@ module (outside this module's edit scope) — until then, sites still supply
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:133-167` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:133-167` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -368,7 +368,7 @@ are disposed; both were verified to fail before the `using` wrappers were added.
|
||||
| Severity | Medium — partially re-triaged: trailing-slash bug fixed; path-templating sub-issue is a design decision (see Resolution) |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:180-196` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:180-196` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -419,7 +419,7 @@ in this finding) is fully resolved.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:167-177` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:167-177` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -459,7 +459,7 @@ size cap alone closes the inflation/disclosure vector.
|
||||
| Severity | Medium — re-triaged: root cause already fixed in current source (see Resolution) |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ErrorClassifier.cs:24-30`, `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:157-159` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ErrorClassifier.cs:24-30`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:157-159` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -513,7 +513,7 @@ the S&F buffer remains empty (the cancelled work is not retried). The existing
|
||||
| Severity | Medium — re-triaged: root cause subsumed by the ExternalSystemGateway-003 dispatch redesign (see Resolution) |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:109-117` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:109-117` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -564,7 +564,7 @@ semantics shared via `InvokeHttpAsync`.
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:48-50` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/DatabaseGateway.cs:48-50` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -600,7 +600,7 @@ exception propagates; it was verified to fail before the `try/catch` was added.
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:360-374`, `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:169-176` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:360-374`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/DatabaseGateway.cs:169-176` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -626,7 +626,7 @@ cleaner of the two options, and one that avoids the staleness hazard a
|
||||
deployment-invalidated cache would introduce.
|
||||
|
||||
Three name-keyed methods were added to `IExternalSystemRepository`
|
||||
(`ScadaLink.Commons`): `GetExternalSystemByNameAsync(name)`,
|
||||
(`ZB.MOM.WW.ScadaBridge.Commons`): `GetExternalSystemByNameAsync(name)`,
|
||||
`GetMethodByNameAsync(externalSystemId, methodName)` and
|
||||
`GetDatabaseConnectionByNameAsync(name)`. The connection lookup belongs on the same
|
||||
interface because database connection definitions are already part of
|
||||
@@ -636,12 +636,12 @@ followed rather than introducing a new interface.
|
||||
|
||||
Both implementers of the interface were updated:
|
||||
|
||||
- `ScadaLink.ConfigurationDatabase.ExternalSystemRepository` — all three are genuine
|
||||
- `ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.ExternalSystemRepository` — all three are genuine
|
||||
server-side keyed queries (`FirstOrDefaultAsync(x => x.Name == name)`, the
|
||||
method lookup additionally scoped by `ExternalSystemDefinitionId`), matching the
|
||||
existing `GetMethodByNameAsync` / `GetListByNameAsync` / `GetSharedScriptByNameAsync`
|
||||
convention in the other Central repositories.
|
||||
- `ScadaLink.SiteRuntime.SiteExternalSystemRepository` — `GetExternalSystemByNameAsync`
|
||||
- `ZB.MOM.WW.ScadaBridge.SiteRuntime.SiteExternalSystemRepository` — `GetExternalSystemByNameAsync`
|
||||
and `GetDatabaseConnectionByNameAsync` are genuine single-row indexed SQLite queries
|
||||
(`WHERE name = @name`; both tables have `name` as the PRIMARY KEY).
|
||||
`GetMethodByNameAsync` resolves the named method from the parent system's
|
||||
@@ -678,9 +678,9 @@ keyed query is deliberately broken):
|
||||
`StubResolution` / `StubConnection` helpers), so the full gateway suite now exercises
|
||||
and protects the keyed-lookup resolution path.
|
||||
|
||||
`dotnet build ScadaLink.slnx` is clean; `ScadaLink.ExternalSystemGateway.Tests` (54),
|
||||
`ScadaLink.ConfigurationDatabase.Tests` (106), `ScadaLink.SiteRuntime.Tests` (196) and
|
||||
`ScadaLink.Commons.Tests` (226) all pass.
|
||||
`dotnet build ZB.MOM.WW.ScadaBridge.slnx` is clean; `ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests` (54),
|
||||
`ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests` (106), `ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests` (196) and
|
||||
`ZB.MOM.WW.ScadaBridge.Commons.Tests` (226) all pass.
|
||||
|
||||
### ExternalSystemGateway-012 — Permanent-failure logging requirement is not met; `_logger` is injected but unused
|
||||
|
||||
@@ -689,7 +689,7 @@ keyed query is deliberately broken):
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:24,169-177`, `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:22` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:24,169-177`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/DatabaseGateway.cs:22` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -734,7 +734,7 @@ against over-logging transient failures).
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemGatewayOptions.cs:9,12`, `src/ScadaLink.ExternalSystemGateway/ServiceCollectionExtensions.cs:13` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemGatewayOptions.cs:9,12`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ServiceCollectionExtensions.cs:13` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -776,7 +776,7 @@ to fail before the wiring was added.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.ExternalSystemGateway.Tests/ExternalSystemClientTests.cs:1`, `tests/ScadaLink.ExternalSystemGateway.Tests/DatabaseGatewayTests.cs` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ExternalSystemClientTests.cs:1`, `tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/DatabaseGatewayTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -834,7 +834,7 @@ against regression.
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:120-127`, `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:102-108` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:120-127`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/DatabaseGateway.cs:102-108` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -907,7 +907,7 @@ message carries the bounded default (99) and never `0`.
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ServiceCollectionExtensions.cs:21-29` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ServiceCollectionExtensions.cs:21-29` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -919,7 +919,7 @@ adds the configuration to *every* `HttpClient`/`IHttpClientFactory` client creat
|
||||
anywhere in the host, regardless of name.
|
||||
|
||||
The Host registers the External System Gateway alongside other components that also
|
||||
use `IHttpClientFactory` — notably `ScadaLink.NotificationService` (`OAuth2TokenService`
|
||||
use `IHttpClientFactory` — notably `ZB.MOM.WW.ScadaBridge.NotificationService` (`OAuth2TokenService`
|
||||
and its `ServiceCollectionExtensions` call `AddHttpClient`). With the ESG registration
|
||||
present, the OAuth2 token client (and any future `HttpClient` consumer in the host)
|
||||
has its **primary handler replaced** by a `SocketsHttpHandler` whose
|
||||
@@ -967,7 +967,7 @@ does; it was verified to fail before the fix.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:324-333` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:324-333` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1004,7 +1004,7 @@ captured request URI has no trailing `?`; it was verified to fail before the fix
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:176`, `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:151` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:176`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/DatabaseGateway.cs:151` |
|
||||
|
||||
**Resolution** — Wrapped the `JsonSerializer.Deserialize<...>(message.PayloadJson)` call in both `ExternalSystemClient.DeliverBufferedAsync` and `DatabaseGateway.DeliverBufferedAsync` in a `try`/`catch (JsonException)` block. A `JsonException` is by definition permanent (the same payload bytes always deserialize identically), so the catch branch logs at `LogError` and returns `false`, parking the message via the S&F engine instead of letting it throw and be retried as a transient failure. Regression tests `DeliverBuffered_MalformedJsonPayload_ReturnsFalseSoMessageParks` were added to both `ExternalSystemClientTests` and `DatabaseGatewayTests` — each feeds a truncated `PayloadJson` to the handler and asserts `delivered == false` and that no exception escapes.
|
||||
|
||||
@@ -1066,7 +1066,7 @@ message parks) and that no exception escapes the handler.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:226,257-264`, `src/ScadaLink.ExternalSystemGateway/ServiceCollectionExtensions.cs:90-102` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:226,257-264`, `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ServiceCollectionExtensions.cs:90-102` |
|
||||
|
||||
**Resolution (2026-05-28):** Set `client.Timeout = Timeout.InfiniteTimeSpan` immediately after `_httpClientFactory.CreateClient($"ExternalSystem_{system.Name}")` in `ExternalSystemClient.InvokeHttpAsync`, disabling the framework's 100 s default so the per-call `CancellationTokenSource(_options.DefaultHttpTimeout)` linked CTS already built below is the sole timeout source. An operator-configured `DefaultHttpTimeout` greater than 100 s is now honoured verbatim instead of being silently clipped and misclassified as a transient "connection error". Kept the fix local to the allowed file (`ExternalSystemClient.cs`) rather than touching `ServiceCollectionExtensions.cs`/`GatewayHttpClientConfigurator`. Regression test `Call_DisablesHttpClientFrameworkTimeoutSoLongTimeoutsArentClipped` asserts the rented client starts with the framework's 100 s default and is set to `Timeout.InfiniteTimeSpan` after `InvokeHttpAsync` runs.
|
||||
|
||||
@@ -1119,7 +1119,7 @@ and produces a `"Timeout calling..."` (not `"Connection error to..."`) error.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/DatabaseGateway.cs:185-193` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/DatabaseGateway.cs:185-193` |
|
||||
|
||||
**Resolution (2026-05-28):** `JsonElementToParameterValue` now probes `TryGetInt64` → `TryGetDecimal` → `GetDouble`, so a JSON number that fits in `decimal` materialises as a `decimal` (preserving the script's authored precision on cached-write retries) and only genuinely out-of-decimal-range values fall through to `double`. Regression test `JsonElementToParameterValue_DecimalShapedNumber_PreservesPrecisionViaDecimal` round-trips `1234567890.1234567890` through a `JsonElement` and asserts the result is a `decimal` carrying the original precision; companion tests guard the long-fast-path and the out-of-range-double fallback.
|
||||
|
||||
@@ -1183,12 +1183,12 @@ that happens to encode a number (already correctly returns `string`).
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:385-415` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:385-415` |
|
||||
|
||||
**Resolution (2026-05-28):** `ApplyAuth` is now an instance method that uses
|
||||
the existing `_logger`. Three previously-silent fail-open paths now emit a
|
||||
`LogWarning` so an operator debugging a recurring 401 sees the cause inside
|
||||
ScadaLink: (1) empty `AuthConfiguration` for `AuthType=apikey`/`basic`,
|
||||
ScadaBridge: (1) empty `AuthConfiguration` for `AuthType=apikey`/`basic`,
|
||||
(2) unknown `AuthType` (anything except `apikey`/`basic`/`none`),
|
||||
(3) malformed Basic config (no `:` separator). The `AuthConfiguration`
|
||||
value is NEVER included in the log message. `AuthType="none"` remains
|
||||
@@ -1217,7 +1217,7 @@ Effectively the gateway treats every misconfiguration as "send anonymously" and
|
||||
relies on the remote system rejecting it with a 401/403. That is a defensible default
|
||||
on its own, but combined with `-007`'s 2 KB error-body cap and the fact that no audit
|
||||
or warning is emitted, an operator debugging "why does my external system always
|
||||
return 401" has nothing to go on inside ScadaLink — the gateway never says it failed
|
||||
return 401" has nothing to go on inside ScadaBridge — the gateway never says it failed
|
||||
to apply auth. For `AuthType = "none"` (the design's expected sentinel for
|
||||
unauthenticated systems) the fall-through is correct; the failure mode is misconfig.
|
||||
|
||||
@@ -1240,7 +1240,7 @@ ever leaked in the warning text would close the test gap as well.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:233` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:233` |
|
||||
|
||||
**Resolution (2026-05-28):** Added a `ValidateHttpMethod` helper called at the top of `InvokeHttpAsync` that rejects any verb outside the documented `GET/POST/PUT/PATCH/DELETE` allowlist (matching ESG-023's design-doc reconciliation) with a clear `ArgumentException` naming the offending verb. Allowlist is a `HashSet<string>` with `OrdinalIgnoreCase` so the operator-authored entity column is case-insensitive. Regression tests `Call_UnsupportedHttpMethod_ThrowsArgumentException` (Theory: FOO/DLETE/GIT/OPTIONS/HEAD) and `Call_DocumentedHttpMethod_IsAccepted` (Theory: GET/get/Post/PATCH/delete) cover the rejection and the case-insensitive accept paths.
|
||||
|
||||
@@ -1279,7 +1279,7 @@ is in the body branch but not the design-doc list; see finding 023).
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ExternalSystemGateway/ExternalSystemClient.cs:241`, `docs/requirements/Component-ExternalSystemGateway.md:43` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs:241`, `docs/requirements/Component-ExternalSystemGateway.md:43` |
|
||||
|
||||
**Resolution (2026-05-28):** Doc-only fix; confirmed PATCH is wired in `ExternalSystemClient.cs:258-260` alongside POST/PUT for body serialization. Added `PATCH` to the design doc's HTTP-method list (line 42) and updated the body/query-parameter sentence (line 75) so the documented set matches the code's `body = POST/PUT/PATCH; query = GET/DELETE` split.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.HealthMonitoring` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring` |
|
||||
| Design doc | `docs/requirements/Component-HealthMonitoring.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -119,7 +119,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs:104`, `src/ScadaLink.HealthMonitoring/HealthReportSender.cs:79` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthCollector.cs:104`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/HealthReportSender.cs:79` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -161,7 +161,7 @@ test. No StoreAndForward source was modified (existing public API only).
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/SiteHealthState.cs:11`, `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:86`, `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:137` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs:11`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:86`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:137` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -209,7 +209,7 @@ and no lost updates.
|
||||
| Severity | Medium — re-triaged: already resolved as a side-effect of HealthMonitoring-002. |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:45-103` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:45-103` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -251,7 +251,7 @@ updates and no torn snapshots. No further code change was required for this find
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:146-148`, `src/ScadaLink.HealthMonitoring/SiteHealthState.cs:21`, `src/ScadaLink.HealthMonitoring/ICentralHealthAggregator.cs:16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:146-148`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs:21`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ICentralHealthAggregator.cs:16` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -289,7 +289,7 @@ number, so readers can reason about the 60s offline grace.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthReportLoop.cs:48-81`, `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:149` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthReportLoop.cs:48-81`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:149` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -333,7 +333,7 @@ offline after 10 minutes) verify the behaviour.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/HealthReportSender.cs:28`, `src/ScadaLink.HealthMonitoring/CentralHealthReportLoop.cs:32` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/HealthReportSender.cs:28`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthReportLoop.cs:32` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -378,7 +378,7 @@ the pre-fix code, which had no `TimeProvider` parameter).
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:86-99` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:86-99` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -422,7 +422,7 @@ verifies the registration; `MarkHeartbeat_KeepsSiteOnline_BetweenReports` and
|
||||
| Severity | Medium — re-triaged: already resolved as a side-effect of HealthMonitoring-002. |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:146-158` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:146-158` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -460,7 +460,7 @@ what the HealthMonitoring-002 change did, so no further code change was required
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.HealthMonitoring.Tests/` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests/` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -502,8 +502,8 @@ Resolved 2026-05-16 (commit `pending`). Added the missing coverage:
|
||||
`SetStoreAndForwardDepths`.
|
||||
|
||||
The `SiteHealthReportReplica` idempotency item is **out of scope** for this module:
|
||||
`SiteHealthReportReplica` is declared in `ScadaLink.Commons` and published/consumed by
|
||||
`CentralCommunicationActor` in the `ScadaLink.Communication` module — the
|
||||
`SiteHealthReportReplica` is declared in `ZB.MOM.WW.ScadaBridge.Commons` and published/consumed by
|
||||
`CentralCommunicationActor` in the `ZB.MOM.WW.ScadaBridge.Communication` module — the
|
||||
HealthMonitoring module itself has no replication code. Replica double-delivery
|
||||
idempotency is already covered by `ProcessReport`'s sequence-number guard
|
||||
(`ProcessReport_RejectsEqualSequence`, `ProcessReport_RejectsStaleReport_WhenSequenceNotGreater`);
|
||||
@@ -517,7 +517,7 @@ The HealthMonitoring test suite now stands at 47 passing tests (was 30).
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/HealthReportSender.cs:70-87` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/HealthReportSender.cs:70-87` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -560,7 +560,7 @@ the pre-fix bare `catch { }` (logged-entry collection was empty).
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/ServiceCollectionExtensions.cs:42-46` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ServiceCollectionExtensions.cs:42-46` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -593,7 +593,7 @@ build and all 50 tests passing.
|
||||
| Severity | Low — re-triaged: already resolved as a side-effect of HealthMonitoring-002. |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/SiteHealthState.cs:11` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs:11` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -620,7 +620,7 @@ HealthMonitoring-007 fixes. `SiteHealthState.LatestReport` is already declared
|
||||
`SiteHealthReport? LatestReport { get; init; }` (the recommendation's "make it properly
|
||||
nullable" option) with an XML doc explaining the `null` case ("known only via heartbeats,
|
||||
has not yet sent a report"). A codebase-wide search confirms no `null!` suppression
|
||||
remains anywhere in `src/ScadaLink.HealthMonitoring`. This is exactly the change
|
||||
remains anywhere in `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring`. This is exactly the change
|
||||
HealthMonitoring-002 made when converting `SiteHealthState` to an immutable record, so
|
||||
the contract is now honest and no further code change was required.
|
||||
|
||||
@@ -631,7 +631,7 @@ the contract is now honest and no further code change was required.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:194-196` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:194-196` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -680,11 +680,11 @@ would have returned 30s against the pre-fix code.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/HealthMonitoringOptions.cs:3-20`, `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:196`, `src/ScadaLink.HealthMonitoring/HealthReportSender.cs:67`, `src/ScadaLink.HealthMonitoring/CentralHealthReportLoop.cs:63` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/HealthMonitoringOptions.cs:3-20`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:196`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/HealthReportSender.cs:67`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthReportLoop.cs:63` |
|
||||
|
||||
**Description**
|
||||
|
||||
`HealthMonitoringOptions` is bound from the `ScadaLink:HealthMonitoring` config
|
||||
`HealthMonitoringOptions` is bound from the `ScadaBridge:HealthMonitoring` config
|
||||
section (`SiteServiceRegistration.BindSharedOptions`) with no validation —
|
||||
no `IValidateOptions<HealthMonitoringOptions>`, no `ValidateDataAnnotations`, no
|
||||
`ValidateOnStart`. `ReportInterval`, `OfflineTimeout`, and `CentralOfflineTimeout`
|
||||
@@ -713,7 +713,7 @@ the hosted service with an opaque `ArgumentOutOfRangeException`. Added
|
||||
`HealthMonitoringOptionsValidator : IValidateOptions<HealthMonitoringOptions>` that
|
||||
rejects non-positive `ReportInterval`/`OfflineTimeout`/`CentralOfflineTimeout` and a
|
||||
`CentralOfflineTimeout` shorter than `OfflineTimeout`, each failure naming the
|
||||
`ScadaLink:HealthMonitoring` config key. It is registered (idempotently, via
|
||||
`ScadaBridge:HealthMonitoring` config key. It is registered (idempotently, via
|
||||
`TryAddEnumerable`) by all three `ServiceCollectionExtensions` registration methods,
|
||||
so it fires when the hosted services resolve `IOptions.Value` at startup — failing
|
||||
fast with a clear message. (`ValidateOnStart()` lives in the Host module's binding
|
||||
@@ -730,7 +730,7 @@ intervals and the `CentralOfflineTimeout < OfflineTimeout` case.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:122-130`, `src/ScadaLink.HealthMonitoring/SiteHealthState.cs:27` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:122-130`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs:27` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -784,7 +784,7 @@ the first would not compile against the pre-fix non-nullable field), and
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs:151` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthCollector.cs:151` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -828,7 +828,7 @@ constructor.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/HealthReportSender.cs:140-154`, `src/ScadaLink.HealthMonitoring/SiteHealthCollector.cs:146-153` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/HealthReportSender.cs:140-154`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthCollector.cs:146-153` |
|
||||
|
||||
**Resolution (2026-05-28):** Wrapped `_transport.Send(reportWithSeq)` in an inner try/catch that, on failure, atomically restores the captured per-interval counts via a new `ISiteHealthCollector.AddIntervalCounters(scriptErrors, alarmErrors, deadLetters, siteAuditWriteFailures, auditRedactionFailures)` API backed by `Interlocked.Add`. Concurrent increments arriving during the Send accumulate against the zero left by `CollectReport`'s `Exchange`; the restore Add sums correctly with them. The new interface method ships with a default no-op so existing test fakes (`CountCapturingHealthCollector` etc.) keep compiling without per-fake updates. Regression test `HealthReportSenderTests.SendFailure_PreservesIntervalCountersForNextReport` pre-populates all five counters, makes the first Send throw, and asserts the next successful report carries the original counts (2 / 1 / 3 / 1 / 2).
|
||||
|
||||
@@ -882,7 +882,7 @@ report includes the previously-failed interval's `ScriptErrorCount`.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthReportLoop.cs:87-98` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthReportLoop.cs:87-98` |
|
||||
|
||||
**Resolution (2026-05-28):** Same shape of fix as HealthMonitoring-017 — `_aggregator.ProcessReport(reportWithSeq)` now sits inside an inner try/catch that, on failure, calls `_collector.AddIntervalCounters(...)` with the captured report's counts. Reuses the same `ISiteHealthCollector.AddIntervalCounters` API; no extra collector surface. Regression test `CentralHealthReportLoopTests.ProcessReportFailure_PreservesIntervalCountersForNextReport` pre-populates all five counters, makes the first `ProcessReport` throw, and asserts the next successful report carries the original counts.
|
||||
|
||||
@@ -919,7 +919,7 @@ collector API once it lands.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `docs/requirements/Component-HealthMonitoring.md:39,40`, `src/ScadaLink.HealthMonitoring/ICentralHealthAggregator.cs`, `src/ScadaLink.AuditLog/Central/AuditCentralHealthSnapshot.cs:39-58` |
|
||||
| Location | `docs/requirements/Component-HealthMonitoring.md:39,40`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ICentralHealthAggregator.cs`, `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditCentralHealthSnapshot.cs:39-58` |
|
||||
|
||||
**Resolution (2026-05-28):** Took the simpler doc-removal path rather than surfacing the metrics through a new HealthMonitoring API. Removed `SiteAuditTelemetryStalled` and `CentralAuditWriteFailures` from the Monitored Metrics table, the Audit Log KPIs section, and the Audit Log Dependencies entry in `Component-HealthMonitoring.md`. The two metrics remain internal to `AuditLog`'s `AuditCentralHealthSnapshot` — promoting them to dashboard tiles is a separate feature, out of scope here.
|
||||
|
||||
@@ -937,11 +937,11 @@ Tracing the code:
|
||||
- `SiteAuditTelemetryStalled` is published by `SiteAuditReconciliationActor`,
|
||||
picked up by `SiteAuditTelemetryStalledTracker`, and latched into
|
||||
`AuditCentralHealthSnapshot._stalled` (a `ConcurrentDictionary<string, bool>`
|
||||
in the `ScadaLink.AuditLog` assembly).
|
||||
in the `ZB.MOM.WW.ScadaBridge.AuditLog` assembly).
|
||||
- `CentralAuditWriteFailures` is incremented inside `AuditCentralHealthSnapshot`
|
||||
via `ICentralAuditWriteFailureCounter.Increment()` (also in `ScadaLink.AuditLog`).
|
||||
via `ICentralAuditWriteFailureCounter.Increment()` (also in `ZB.MOM.WW.ScadaBridge.AuditLog`).
|
||||
|
||||
Neither metric is referenced anywhere in `src/ScadaLink.HealthMonitoring/`:
|
||||
Neither metric is referenced anywhere in `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/`:
|
||||
- `ICentralHealthAggregator` does not expose them.
|
||||
- `SiteHealthCollector` has no central counterpart (it is site-only).
|
||||
- `SiteHealthReport` has no `SiteAuditTelemetryStalled` / `CentralAuditWriteFailures`
|
||||
@@ -955,7 +955,7 @@ design doc places these metrics under HealthMonitoring's responsibility
|
||||
Dependencies section's claim that Health Monitoring provides "the
|
||||
central-computed `CentralAuditWriteFailures` / `AuditRedactionFailure` metrics"
|
||||
is false for `CentralAuditWriteFailures`: nothing under
|
||||
`src/ScadaLink.HealthMonitoring/` knows about it.
|
||||
`src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/` knows about it.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
@@ -980,7 +980,7 @@ counter and per-site stalled state.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:128-147` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:128-147` |
|
||||
|
||||
**Resolution (2026-05-28):** `MarkHeartbeat` now branches on
|
||||
`existing.IsOnline`: when transitioning offline-to-online it anchors
|
||||
@@ -1035,7 +1035,7 @@ online.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.HealthMonitoring/CentralHealthReportLoop.cs:22`, `src/ScadaLink.HealthMonitoring/CentralHealthAggregator.cs:224-226` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthReportLoop.cs:22`, `src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/CentralHealthAggregator.cs:224-226` |
|
||||
|
||||
**Resolution (2026-05-28):** `CentralHealthReportLoop.CentralSiteId` is now `"$central"` instead of `"central"`. The leading `$` is forbidden in operator-set `Site.SiteIdentifier` values (which are plain identifiers), so the synthetic central self-report cannot collide with a real site whose identifier happens to be the bare word `"central"`. The collision case the finding called out — two reports clobbering each other in the aggregator keyspace via the sequence-number guard and a real site inheriting `CentralOfflineTimeout` and staying falsely-online for an extra two minutes — is now impossible. The aggregator (`CentralHealthAggregator.CheckForOfflineSites`), the Central UI health dashboard (`Monitoring/Health.razor`), and every test reference the constant rather than the literal string, so the value change is local — no consumer code needed updating. Existing `CentralHealthAggregatorTests` and `CentralHealthReportLoopTests` already use the constant, so they continue to pin the central-self-report identity through the new sentinel.
|
||||
|
||||
@@ -1088,9 +1088,9 @@ preserved.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.HealthMonitoring.Tests/CentralHealthReportLoopTests.cs:32-42` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests/CentralHealthReportLoopTests.cs:32-42` |
|
||||
|
||||
**Resolution (2026-05-28):** Picked the less-invasive recommended option (a) — kept the real-time `PeriodicTimer` but replaced the fixed-budget `Task.Delay` with a generous poll-until-condition helper. `RunLoopUntil(loop, condition, maxWait = 5s)` starts the hosted service, polls `condition` every 25 ms with a 5 s outer cap, and stops cleanly when met; `GeneratesCentralReports_WhenSelfIsPrimary`, `AssignsMonotonicSequenceNumbers`, and `ProcessReportFailure_PreservesIntervalCountersForNextReport` now use it. The legacy `RunLoopBriefly` retains a fixed wait (≥ 1 s) for the two tests that assert *absence* of reports (`GeneratesNoReports_WhenNotPrimary`, `SetsActiveNodeFlag_EvenWhenNotPrimary`) since there is no condition to poll for. Refactoring the production loop to consume `TimeProvider.CreateTimer` (option b) was rejected for batch scope — it would require a production change for what is currently a low-severity test-hygiene gap. All 73 `ScadaLink.HealthMonitoring.Tests` pass; the new generous budget tolerates slow CI runners.
|
||||
**Resolution (2026-05-28):** Picked the less-invasive recommended option (a) — kept the real-time `PeriodicTimer` but replaced the fixed-budget `Task.Delay` with a generous poll-until-condition helper. `RunLoopUntil(loop, condition, maxWait = 5s)` starts the hosted service, polls `condition` every 25 ms with a 5 s outer cap, and stops cleanly when met; `GeneratesCentralReports_WhenSelfIsPrimary`, `AssignsMonotonicSequenceNumbers`, and `ProcessReportFailure_PreservesIntervalCountersForNextReport` now use it. The legacy `RunLoopBriefly` retains a fixed wait (≥ 1 s) for the two tests that assert *absence* of reports (`GeneratesNoReports_WhenNotPrimary`, `SetsActiveNodeFlag_EvenWhenNotPrimary`) since there is no condition to poll for. Refactoring the production loop to consume `TimeProvider.CreateTimer` (option b) was rejected for batch scope — it would require a production change for what is currently a low-severity test-hygiene gap. All 73 `ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests` pass; the new generous budget tolerates slow CI runners.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1125,7 +1125,7 @@ module's tests.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.HealthMonitoring.Tests/SiteHealthCollectorTests.cs:117-122` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests/SiteHealthCollectorTests.cs:117-122` |
|
||||
|
||||
**Resolution (2026-05-28):** Renamed the test method from `StoreAndForwardBufferDepths_IsEmptyPlaceholder` to `StoreAndForwardBufferDepths_DefaultsToEmpty_WhenSetterNotCalled` so the name describes what it actually asserts — the default-state contract of a fresh collector. No behaviour change; the body still constructs a collector without calling `SetStoreAndForwardDepths` and asserts `Empty(report.StoreAndForwardBufferDepths)`.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.Host` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.Host` |
|
||||
| Design doc | `docs/requirements/Component-Host.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
## Summary
|
||||
|
||||
The Host module is the composition root for the entire ScadaLink system: a single
|
||||
The Host module is the composition root for the entire ScadaBridge system: a single
|
||||
binary whose behaviour (`Central` vs `Site`) is driven entirely by configuration. The
|
||||
implementation is generally faithful to `Component-Host.md` — startup validation,
|
||||
role-based registration, Serilog enrichment, Windows Service support, dead-letter
|
||||
monitoring, CoordinatedShutdown, and gRPC hosting on site nodes are all present and
|
||||
backed by a solid test suite (`tests/ScadaLink.Host.Tests`).
|
||||
backed by a solid test suite (`tests/ZB.MOM.WW.ScadaBridge.Host.Tests`).
|
||||
|
||||
The most significant problem is the readiness endpoint: `/health/ready` runs **all**
|
||||
registered health checks, including the leader-only `active-node` check, so a fully
|
||||
@@ -74,7 +74,7 @@ call passes `default` for `CancellationToken`, so a SIGTERM during the
|
||||
bounded-retry window is ignored for up to ~2 minutes (Host-019);
|
||||
`LoggerConfigurationFactory` layers `MinimumLevel.Is` over
|
||||
`ReadFrom.Configuration`, so any `Serilog:MinimumLevel` an operator sets is
|
||||
silently overridden by `ScadaLink:Logging:MinimumLevel` (Host-020); the
|
||||
silently overridden by `ScadaBridge:Logging:MinimumLevel` (Host-020); the
|
||||
shipped `appsettings.json` carries a Microsoft `Logging:LogLevel` block but
|
||||
Serilog is the only logger provider and the section is dead config (Host-021);
|
||||
and `ParseLevel` silently swallows an unrecognised `MinimumLevel` value (e.g.
|
||||
@@ -108,7 +108,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| 7 | Design-document adherence | ☑ | Re-review: REQ-HOST-7 site-shutdown ordering — stop accepting new streams, cancel active streams via `ApplicationStopping`, then tear down actors — is not wired in `Program.cs` (Host-017). |
|
||||
| 8 | Code organization & conventions | ☑ | Re-review: `NodeOptions.NodeName` is absent from the shipped per-role configs even though it stamps `AuditLog.SourceNode` (Host-018); the appsettings `Logging:LogLevel` Microsoft section is dead config under Serilog (Host-021). |
|
||||
| 9 | Testing coverage | ☑ | Strong existing suite. No coverage for the Site `CentralContactPoints` second-entry rule (Host-016), the site-shutdown ordering (Host-017), the `NodeName`-absent shipped config (Host-018), the unused `CancellationToken` parameter (Host-019), the `MinimumLevel.Is` override semantics (Host-020) or the `ParseLevel` silent fallback (Host-022). |
|
||||
| 10 | Documentation & comments | ☑ | Re-review: layered `MinimumLevel.Is` / `ReadFrom.Configuration` semantics are not surfaced — an operator-set `Serilog:MinimumLevel` is silently overridden by `ScadaLink:Logging:MinimumLevel` (Host-020); `ParseLevel` silently coerces a misspelled level to `Information` with no warning (Host-022). |
|
||||
| 10 | Documentation & comments | ☑ | Re-review: layered `MinimumLevel.Is` / `ReadFrom.Configuration` semantics are not surfaced — an operator-set `Serilog:MinimumLevel` is silently overridden by `ScadaBridge:Logging:MinimumLevel` (Host-020); `ParseLevel` silently coerces a misspelled level to `Information` with no warning (Host-022). |
|
||||
|
||||
## Findings
|
||||
|
||||
@@ -119,7 +119,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:135-145` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:135-145` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -165,7 +165,7 @@ response body; it failed before the fix and passes after. Full Host suite green
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:70-108`, `docs/requirements/Component-Host.md` REQ-HOST-6 |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:70-108`, `docs/requirements/Component-Host.md` REQ-HOST-6 |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -173,7 +173,7 @@ REQ-HOST-6 states the Host "must configure the Akka.NET actor system using
|
||||
Akka.Hosting with ... **Persistence**: Configured with the appropriate journal and
|
||||
snapshot store (SQL for central, SQLite for site)." The HOCON built in
|
||||
`AkkaHostedService.StartAsync` contains no `akka.persistence` section, no journal and
|
||||
no snapshot-store plugin, and `ScadaLink.Host.csproj` references neither
|
||||
no snapshot-store plugin, and `ZB.MOM.WW.ScadaBridge.Host.csproj` references neither
|
||||
`Akka.Persistence.Hosting` nor any persistence plugin (the design doc Dependencies
|
||||
list `Akka.Persistence.Hosting`). A repo-wide search finds **no** `PersistentActor` /
|
||||
`ReceivePersistentActor` subclasses — the system deliberately uses custom SQLite
|
||||
@@ -193,7 +193,7 @@ add the plugin packages and HOCON. Either way, code and doc must agree.
|
||||
Resolved 2026-05-16 (commit `<pending>`). Confirmed accurate: a repo-wide search
|
||||
finds **no** `PersistentActor` / `ReceivePersistentActor` subclasses anywhere in
|
||||
`src/`, no `akka.persistence` section in the HOCON built by
|
||||
`AkkaHostedService.StartAsync`, and `ScadaLink.Host.csproj` references no
|
||||
`AkkaHostedService.StartAsync`, and `ZB.MOM.WW.ScadaBridge.Host.csproj` references no
|
||||
persistence plugin packages. The Host code is correct and internally consistent;
|
||||
the defect was a stale design doc. Fix: updated `docs/requirements/Component-Host.md`
|
||||
— REQ-HOST-6 no longer lists Persistence as a configured subsystem and now carries
|
||||
@@ -211,15 +211,15 @@ agree; no code or test change required.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.Central.json:20-31` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Central.json:20-31` |
|
||||
|
||||
**Description**
|
||||
|
||||
`appsettings.Central.json` contains real-looking secrets in plaintext, checked into
|
||||
source control: SQL Server passwords in the `ConfigurationDb` / `MachineDataDb`
|
||||
connection strings (`Password=ScadaLink_Dev1#`), an LDAP service-account password
|
||||
connection strings (`Password=ScadaBridge_Dev1#`), an LDAP service-account password
|
||||
(`LdapServiceAccountPassword: "password"`), and a JWT signing key
|
||||
(`JwtSigningKey: "scadalink-dev-jwt-signing-key-..."`). Even though these are
|
||||
(`JwtSigningKey: "scadabridge-dev-jwt-signing-key-..."`). Even though these are
|
||||
intended as development defaults, shipping them in the default config invites them
|
||||
being reused verbatim in production, and a committed JWT signing key allows anyone
|
||||
with repo access to forge session tokens. `TrustServerCertificate=true` additionally
|
||||
@@ -237,16 +237,16 @@ environment.
|
||||
|
||||
Resolved 2026-05-16 (commit `pending`). Root cause confirmed against
|
||||
`appsettings.Central.json`: the committed file carried real-looking secrets in
|
||||
plaintext — SQL Server passwords (`Password=ScadaLink_Dev1#`) in both connection
|
||||
plaintext — SQL Server passwords (`Password=ScadaBridge_Dev1#`) in both connection
|
||||
strings, an LDAP service-account password, and a JWT signing key. Fix: all four
|
||||
secrets were removed from the committed file and replaced with non-functional
|
||||
`${...}` placeholder references (`ConfigurationDb` / `MachineDataDb`,
|
||||
`LdapServiceAccountPassword`, `JwtSigningKey`). A new top-level `_secrets` note
|
||||
documents that the Host's configuration builder (`AddEnvironmentVariables()`)
|
||||
overlays the real values supplied via environment variables
|
||||
(`ScadaLink__Database__ConfigurationDb`, `ScadaLink__Database__MachineDataDb`,
|
||||
`ScadaLink__Security__LdapServiceAccountPassword`,
|
||||
`ScadaLink__Security__JwtSigningKey`); the placeholders are intentionally invalid so
|
||||
(`ScadaBridge__Database__ConfigurationDb`, `ScadaBridge__Database__MachineDataDb`,
|
||||
`ScadaBridge__Security__LdapServiceAccountPassword`,
|
||||
`ScadaBridge__Security__JwtSigningKey`); the placeholders are intentionally invalid so
|
||||
a misconfigured deployment fails loudly rather than silently using a committed key.
|
||||
Regression test class `ConfigSecretsTests` asserts the committed file carries no
|
||||
plaintext `Password=` value, no committed LDAP service-account password, and no
|
||||
@@ -266,13 +266,13 @@ scope; they should receive the same treatment in a follow-up.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.Site.json:10-19` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json:10-19` |
|
||||
|
||||
**Description**
|
||||
|
||||
The shipped site config sets `Node:RemotingPort = 8082` and `Node:GrpcPort = 8083`,
|
||||
but `Cluster:SeedNodes` is `["akka.tcp://scadalink@localhost:8082",
|
||||
"akka.tcp://scadalink@localhost:8083"]`. The second seed node targets `8083`, which
|
||||
but `Cluster:SeedNodes` is `["akka.tcp://scadabridge@localhost:8082",
|
||||
"akka.tcp://scadabridge@localhost:8083"]`. The second seed node targets `8083`, which
|
||||
is the Kestrel HTTP/2 gRPC port — not an Akka remoting endpoint. A node attempting to
|
||||
join via that seed will try to establish an Akka.Remote TCP association against the
|
||||
gRPC listener and fail. `StartupValidator` only checks that ≥2 seed nodes exist
|
||||
@@ -290,7 +290,7 @@ to reject a seed node whose port equals this node's `GrpcPort`.
|
||||
|
||||
Resolved 2026-05-16 (commit `pending`). Root cause confirmed against
|
||||
`appsettings.Site.json`: with `Node:RemotingPort = 8082` and `Node:GrpcPort = 8083`,
|
||||
the second `Cluster:SeedNodes` entry was `akka.tcp://scadalink@localhost:8083` — the
|
||||
the second `Cluster:SeedNodes` entry was `akka.tcp://scadabridge@localhost:8083` — the
|
||||
Kestrel HTTP/2 gRPC port, not an Akka.Remote endpoint. `StartupValidator` only
|
||||
checked seed-node *count* (≥2), so the misconfiguration passed silently. Fix, two
|
||||
parts: (1) the shipped `appsettings.Site.json` second seed entry was corrected to a
|
||||
@@ -313,7 +313,7 @@ appropriately before the fix and pass after.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:345` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:345` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -354,7 +354,7 @@ async startup pipeline and remain green (175 passed).
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:70-108` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:70-108` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -404,7 +404,7 @@ suite green (175 passed).
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/StartupValidator.cs:43-47` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:43-47` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -421,7 +421,7 @@ piece.
|
||||
|
||||
Add a check in the `role == "Site"` block: if `GrpcPort` (resolved, including the
|
||||
8083 default) equals `RemotingPort`, add an error
|
||||
`"ScadaLink:Node:GrpcPort must differ from RemotingPort"`.
|
||||
`"ScadaBridge:Node:GrpcPort must differ from RemotingPort"`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
@@ -429,7 +429,7 @@ Resolved 2026-05-16 (commit `pending`). Root cause confirmed against
|
||||
`StartupValidator.cs`: the `role == "Site"` block validated the GrpcPort range but
|
||||
never compared it to `RemotingPort`, so a site config setting both ports equal
|
||||
passed validation and then failed opaquely when Kestrel and Akka.Remote both bound
|
||||
the port. Fix: added `if (port == grpcPort) errors.Add("ScadaLink:Node:GrpcPort must
|
||||
the port. Fix: added `if (port == grpcPort) errors.Add("ScadaBridge:Node:GrpcPort must
|
||||
differ from RemotingPort")` in the Site block, using the resolved GrpcPort (including
|
||||
the 8083 `NodeOptions` default when the key is absent) against the parsed
|
||||
RemotingPort. Regression tests in `StartupValidatorTests`:
|
||||
@@ -445,11 +445,11 @@ before the fix and pass after. Full Host suite green (175 passed).
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/StartupValidator.cs:33-34`, `src/ScadaLink.Host/DatabaseOptions.cs:6` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:33-34`, `src/ZB.MOM.WW.ScadaBridge.Host/DatabaseOptions.cs:6` |
|
||||
|
||||
**Description**
|
||||
|
||||
`StartupValidator` requires a non-empty `ScadaLink:Database:MachineDataDb` connection
|
||||
`StartupValidator` requires a non-empty `ScadaBridge:Database:MachineDataDb` connection
|
||||
string for Central nodes, and `DatabaseOptions` exposes a `MachineDataDb` property,
|
||||
but a repo-wide search shows the value is never read anywhere outside the Host module
|
||||
— only `ConfigurationDb` is passed to `AddConfigurationDatabase`
|
||||
@@ -489,7 +489,7 @@ key harmlessly — it will simply be ignored; a follow-up may remove it for tidi
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:127-141` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:127-141` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -540,7 +540,7 @@ Host suite green (175 passed).
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:112-125` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:112-125` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -585,40 +585,40 @@ the pre-fix code (the helper did not exist) and pass after. Full Host suite gree
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/LoggingOptions.cs:5`, `src/ScadaLink.Host/Program.cs:42-50` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/LoggingOptions.cs:5`, `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:42-50` |
|
||||
|
||||
**Description**
|
||||
|
||||
`LoggingOptions` exposes a `MinimumLevel` property bound from `ScadaLink:Logging`
|
||||
`LoggingOptions` exposes a `MinimumLevel` property bound from `ScadaBridge:Logging`
|
||||
(`SiteServiceRegistration.BindSharedOptions`), and both `appsettings.Central.json`
|
||||
and `appsettings.Site.json` set `"Logging": { "MinimumLevel": "Information" }`.
|
||||
However Serilog is configured purely via `ReadFrom.Configuration(configuration)`,
|
||||
which reads the standard `Serilog` section — not `ScadaLink:Logging`. The
|
||||
which reads the standard `Serilog` section — not `ScadaBridge:Logging`. The
|
||||
`LoggingOptions.MinimumLevel` value is never read by any code, so changing it has no
|
||||
effect. This is misleading: an operator editing `ScadaLink:Logging:MinimumLevel`
|
||||
effect. This is misleading: an operator editing `ScadaBridge:Logging:MinimumLevel`
|
||||
expecting a log-level change will see nothing happen.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Either consume `LoggingOptions.MinimumLevel` when configuring the Serilog
|
||||
`LoggerConfiguration` (e.g. set `MinimumLevel.Is(...)` from it), or remove the option
|
||||
class and the `ScadaLink:Logging` sections and rely solely on the `Serilog`
|
||||
class and the `ScadaBridge:Logging` sections and rely solely on the `Serilog`
|
||||
configuration section. Keep one mechanism, not two.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-16 (commit `pending`). Root cause confirmed: `LoggingOptions.MinimumLevel`
|
||||
was bound from `ScadaLink:Logging` but Serilog was configured purely via
|
||||
was bound from `ScadaBridge:Logging` but Serilog was configured purely via
|
||||
`ReadFrom.Configuration` (standard `Serilog` section), so editing
|
||||
`ScadaLink:Logging:MinimumLevel` had no effect. Re-triaged the two options: the
|
||||
`ScadaBridge:Logging:MinimumLevel` had no effect. Re-triaged the two options: the
|
||||
"remove the option class" branch was rejected because `Component-Host.md` REQ-HOST-3
|
||||
explicitly lists `ScadaLink:Logging` → `LoggingOptions` and that design doc is outside
|
||||
explicitly lists `ScadaBridge:Logging` → `LoggingOptions` and that design doc is outside
|
||||
this task's edit scope — removing the class would create a fresh code-vs-doc drift.
|
||||
Fix (the "consume it" branch): added a `LoggerConfigurationFactory.Build` that binds
|
||||
`LoggingOptions`, parses `MinimumLevel` into a Serilog `LogEventLevel` (falling back
|
||||
to `Information` for null/blank/unrecognised values), and pins it via
|
||||
`MinimumLevel.Is(...)` on top of `ReadFrom.Configuration`; `Program.cs` now builds the
|
||||
logger through this factory. `ScadaLink:Logging:MinimumLevel` is now live.
|
||||
logger through this factory. `ScadaBridge:Logging:MinimumLevel` is now live.
|
||||
Regression tests in new `LoggerConfigurationTests`:
|
||||
`MinimumLevel_Warning_SuppressesInformationLogs`,
|
||||
`MinimumLevel_Debug_AllowsDebugLogs`, and `MinimumLevel_Absent_DefaultsToInformation`
|
||||
@@ -633,17 +633,17 @@ key). Full Host suite green (175 passed).
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:146-148` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:146-148` |
|
||||
|
||||
**Description**
|
||||
|
||||
`AkkaHostedService.BuildHocon` emits the split-brain-resolver block with
|
||||
`keep-oldest { down-if-alone = on }` as a literal constant. `ClusterOptions`
|
||||
(`src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:74`) exposes a
|
||||
`DownIfAlone` property — bound from `ScadaLink:Cluster` via the Options pattern,
|
||||
(`src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:74`) exposes a
|
||||
`DownIfAlone` property — bound from `ScadaBridge:Cluster` via the Options pattern,
|
||||
documented as "the design-doc requirement", default `true` — but a repo-wide search
|
||||
shows it is referenced **nowhere outside its own declaration**. The Host therefore
|
||||
ignores the bound value entirely: setting `ScadaLink:Cluster:DownIfAlone` to `false`
|
||||
ignores the bound value entirely: setting `ScadaBridge:Cluster:DownIfAlone` to `false`
|
||||
in `appsettings.json` has no effect, the resolver still runs with `down-if-alone =
|
||||
on`. This is the same class of defect as the resolved Host-011
|
||||
(`LoggingOptions.MinimumLevel` was dead config) — a configuration option that is
|
||||
@@ -659,7 +659,7 @@ Make `BuildHocon` consume `clusterOptions.DownIfAlone` — emit `down-if-alone =
|
||||
{(clusterOptions.DownIfAlone ? "on" : "off")}` (the value is a bool, so no escaping
|
||||
is needed). Add a `HoconBuilderTests` case asserting both `true` and `false` produce
|
||||
the corresponding `down-if-alone` token. If the flag is genuinely meant to be fixed
|
||||
at `on` for ScadaLink's two-node clusters, remove the `DownIfAlone` property and its
|
||||
at `on` for ScadaBridge's two-node clusters, remove the `DownIfAlone` property and its
|
||||
doc comment instead so code and configuration contract agree — but do not leave it
|
||||
declared-but-dead.
|
||||
|
||||
@@ -682,7 +682,7 @@ the fix (token was still `on`) and passes after. Full Host suite green (182 pass
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:135-136,145,151-152` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:135-136,145,151-152` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -727,7 +727,7 @@ after. Full Host suite green (182 passed).
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:43-48`, `src/ScadaLink.Host/appsettings.json:1-7` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:43-48`, `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.json:1-7` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -738,7 +738,7 @@ configuration section — but neither `appsettings.json` nor either role-specifi
|
||||
contains a `Serilog` section (only a Microsoft `Logging` section with a `LogLevel`
|
||||
map, which Serilog's `ReadFrom.Configuration` does not consume). The two sinks that
|
||||
actually run are appended in `Program.cs` as hard-coded `.WriteTo.Console(...)` and
|
||||
`.WriteTo.File("logs/scadalink-.log", rollingInterval: Day)` calls. As a result the
|
||||
`.WriteTo.File("logs/scadabridge-.log", rollingInterval: Day)` calls. As a result the
|
||||
console output template, the file path, and the rolling interval cannot be changed
|
||||
without recompiling — an operator cannot redirect logs, change the file location, or
|
||||
add a sink via configuration, contrary to REQ-HOST-8. The `ReadFrom.Configuration`
|
||||
@@ -761,7 +761,7 @@ role-specific file contained a `Serilog` section, so the two sinks that actually
|
||||
were hard-coded `.WriteTo.Console(...)` / `.WriteTo.File(...)` calls in `Program.cs`
|
||||
— uneditable without recompiling, contrary to REQ-HOST-8. Fix: added a `Serilog`
|
||||
section to `appsettings.json` with `WriteTo` entries for the Console and File sinks
|
||||
(carrying the output template, file path `logs/scadalink-.log` and `Day` rolling
|
||||
(carrying the output template, file path `logs/scadabridge-.log` and `Day` rolling
|
||||
interval as args) and removed the hard-coded `.WriteTo` calls from `Program.cs`; the
|
||||
factory's `ReadFrom.Configuration` (backed by the transitive
|
||||
`Serilog.Settings.Configuration` package) now drives the sinks. Regression tests in
|
||||
@@ -779,7 +779,7 @@ green (182 passed).
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/StartupRetry.cs:36-45` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/StartupRetry.cs:36-45` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -832,13 +832,13 @@ Full Host suite green (182 passed).
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.Site.json:33-37` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json:33-37` |
|
||||
|
||||
**Description**
|
||||
|
||||
The shipped site config sets `Node:RemotingPort = 8082` and lists
|
||||
`Communication:CentralContactPoints` as
|
||||
`["akka.tcp://scadalink@localhost:8081", "akka.tcp://scadalink@localhost:8082"]`.
|
||||
`["akka.tcp://scadabridge@localhost:8081", "akka.tcp://scadabridge@localhost:8082"]`.
|
||||
The second contact point — port `8082` — is the **site's own** remoting endpoint,
|
||||
not a central node. `SiteCommunicationActor` / `ClusterClient` uses these
|
||||
addresses as initial contacts when discovering the central
|
||||
@@ -861,7 +861,7 @@ multi-node layout). Consider extending `StartupValidator` to reject any
|
||||
node's `NodeHostname`+`RemotingPort`. Add a regression test in
|
||||
`StartupValidatorTests` mirroring `Site_SeedNodeOnGrpcPort_FailsValidation`.
|
||||
|
||||
**Resolution (2026-05-28):** The shipped `appsettings.Site.json` `CentralContactPoints` entry that pointed at the site's own remoting port (`localhost:8082`) was removed — the dev-loopback default now lists only the single central node (`akka.tcp://scadalink@localhost:8081`), which is the actually-reachable target in the single-node dev layout. A `_centralContactPoints` doc-key comment was added immediately above the array calling out the per-entry rule (each entry MUST be a central node's remoting endpoint, not the site's own remoting port) and explaining how to extend the list with a second central node (`akka.tcp://scadalink@central-b-host:8081`) in a multi-central deployment so ClusterClient can fail over. The dangerous example pattern that would have been copied into multi-central configs no longer exists in the template. `StartupValidator` cross-check is left as a follow-up — the documented rule plus the corrected template removes the immediate misconfiguration risk.
|
||||
**Resolution (2026-05-28):** The shipped `appsettings.Site.json` `CentralContactPoints` entry that pointed at the site's own remoting port (`localhost:8082`) was removed — the dev-loopback default now lists only the single central node (`akka.tcp://scadabridge@localhost:8081`), which is the actually-reachable target in the single-node dev layout. A `_centralContactPoints` doc-key comment was added immediately above the array calling out the per-entry rule (each entry MUST be a central node's remoting endpoint, not the site's own remoting port) and explaining how to extend the list with a second central node (`akka.tcp://scadabridge@central-b-host:8081`) in a multi-central deployment so ClusterClient can fail over. The dangerous example pattern that would have been copied into multi-central configs no longer exists in the template. `StartupValidator` cross-check is left as a follow-up — the documented rule plus the corrected template removes the immediate misconfiguration risk.
|
||||
|
||||
### Host-017 — Site-shutdown ordering from REQ-HOST-7 is not wired
|
||||
|
||||
@@ -870,7 +870,7 @@ node's `NodeHostname`+`RemotingPort`. Add a regression test in
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:229-265`, `src/ScadaLink.Communication/Grpc/SiteStreamGrpcServer.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:229-265`, `src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -904,7 +904,7 @@ before the Akka hosted service runs `CoordinatedShutdown` (or order via
|
||||
reverse-registration order, so the gRPC server's lifetime can be sequenced
|
||||
before Akka shutdown). Alternatively, reconcile REQ-HOST-7 with the actual
|
||||
implementation if the explicit ordering is no longer intended. Add an
|
||||
integration test under `tests/ScadaLink.Host.Tests` that starts a site host,
|
||||
integration test under `tests/ZB.MOM.WW.ScadaBridge.Host.Tests` that starts a site host,
|
||||
opens a stream, triggers shutdown, and asserts the stream completes with
|
||||
`Cancelled` before the actor system tears down.
|
||||
|
||||
@@ -928,7 +928,7 @@ ordering. Clients observe a clean `Cancelled` and reconnect rather than a
|
||||
silent stream that times out via keepalive (~25 s).
|
||||
|
||||
Two unit regression tests added to
|
||||
`tests/ScadaLink.Communication.Tests/Grpc/SiteStreamGrpcServerTests.cs`:
|
||||
`tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/Grpc/SiteStreamGrpcServerTests.cs`:
|
||||
`Host017_CancelAllStreams_CancelsActiveStreamsAndRefusesNewOnes` (active
|
||||
streams complete, new ones rejected) and `Host017_CancelAllStreams_IsIdempotent`
|
||||
(double-call safe). A full site-host integration test was deferred — the
|
||||
@@ -942,7 +942,7 @@ unit suite covers both server-side invariants and the wiring is a single
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.Central.json`, `src/ScadaLink.Host/appsettings.Site.json`, `src/ScadaLink.Host/NodeOptions.cs:10-16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Central.json`, `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Site.json`, `src/ZB.MOM.WW.ScadaBridge.Host/NodeOptions.cs:10-16` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -955,8 +955,8 @@ telemetry and reconciliation, and is indexed via
|
||||
`IX_AuditLog_Node_Occurred (SourceNode, OccurredAtUtc)`. The docker per-node
|
||||
configs (`docker/central-node-a/appsettings.Central.json`,
|
||||
`docker/site-a-node-a/appsettings.Site.json`, etc.) all set
|
||||
`ScadaLink:Node:NodeName`. The **shipped, default** per-role files in
|
||||
`src/ScadaLink.Host/` — the templates a developer running the binary
|
||||
`ScadaBridge:Node:NodeName`. The **shipped, default** per-role files in
|
||||
`src/ZB.MOM.WW.ScadaBridge.Host/` — the templates a developer running the binary
|
||||
directly will use — do not. `NodeIdentityProvider` normalises an empty
|
||||
`NodeName` to `null`, so dev audit rows carry a null `SourceNode` and the
|
||||
indexed lookup never narrows. The dev examples should match the docker
|
||||
@@ -972,7 +972,7 @@ per-node in multi-node deployments. Consider validating in `StartupValidator`
|
||||
that `NodeName` is non-empty, or accept the null and document explicitly that
|
||||
single-node dev deployments leave `SourceNode` null.
|
||||
|
||||
**Resolution (2026-05-28):** The shipped per-role templates now set `ScadaLink:Node:NodeName` — `central-a` in `appsettings.Central.json` and `node-a` in `appsettings.Site.json` — so dev audit rows are stamped with a real `SourceNode` value (instead of `NodeIdentityProvider` normalising the missing key to `null`) and the indexed `IX_AuditLog_Node_Occurred` lookup actually narrows. A `_nodeName` doc-key comment was added beside each `Node` section explaining the convention (`central-a`/`central-b` for central, `node-a`/`node-b` for site), pointing at the docker per-node configs (which already overrode the field), and noting that the value must be overridden per-node in multi-node deployments and that an empty value still normalises to a `NULL` SourceNode. The shipped dev templates now match the per-node docker examples — a developer running the binary directly no longer sees a null `SourceNode`.
|
||||
**Resolution (2026-05-28):** The shipped per-role templates now set `ScadaBridge:Node:NodeName` — `central-a` in `appsettings.Central.json` and `node-a` in `appsettings.Site.json` — so dev audit rows are stamped with a real `SourceNode` value (instead of `NodeIdentityProvider` normalising the missing key to `null`) and the indexed `IX_AuditLog_Node_Occurred` lookup actually narrows. A `_nodeName` doc-key comment was added beside each `Node` section explaining the convention (`central-a`/`central-b` for central, `node-a`/`node-b` for site), pointing at the docker per-node configs (which already overrode the field), and noting that the value must be overridden per-node in multi-node deployments and that an empty value still normalises to a `NULL` SourceNode. The shipped dev templates now match the per-node docker examples — a developer running the binary directly no longer sees a null `SourceNode`.
|
||||
|
||||
### Host-019 — Migration `StartupRetry` call drops the host `CancellationToken`
|
||||
|
||||
@@ -981,7 +981,7 @@ single-node dev deployments leave `SourceNode` null.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:154-165` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:154-165` |
|
||||
|
||||
**Resolution (2026-05-28):** Added a `Func<CancellationToken, Task>` overload of `StartupRetry.ExecuteWithRetryAsync` that forwards the retry-loop token into the operation, and the migration call site in `Program.cs` now passes `app.Lifetime.ApplicationStopping` as both the operation token (threaded to `MigrationHelper.ApplyOrValidateMigrationsAsync`) and the loop's `cancellationToken` (already honoured by the inter-attempt `Task.Delay`). A SIGTERM during the bounded retry window now tears down cleanly instead of waiting up to ~2 minutes for the loop to exhaust. The original `Func<Task>` overload still exists and delegates, so existing callers/tests are unchanged.
|
||||
|
||||
@@ -1018,7 +1018,7 @@ _Open._
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/LoggerConfigurationFactory.cs:36-43` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/LoggerConfigurationFactory.cs:36-43` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1026,10 +1026,10 @@ _Open._
|
||||
via `ReadFrom.Configuration(configuration)` (which can include a
|
||||
`MinimumLevel` block — the standard Serilog way to set the floor) and **then**
|
||||
calls `.MinimumLevel.Is(minimumLevel)` derived from
|
||||
`ScadaLink:Logging:MinimumLevel`. Serilog's fluent builder applies the later
|
||||
`ScadaBridge:Logging:MinimumLevel`. Serilog's fluent builder applies the later
|
||||
call, so any `Serilog:MinimumLevel:Default` an operator sets is silently
|
||||
overridden by `ScadaLink:Logging:MinimumLevel` (or by its
|
||||
`Information` fallback when the ScadaLink key is absent). There are now two
|
||||
overridden by `ScadaBridge:Logging:MinimumLevel` (or by its
|
||||
`Information` fallback when the ScadaBridge key is absent). There are now two
|
||||
documented configuration paths for the same setting with non-obvious
|
||||
precedence, and the override direction is the opposite of what most Serilog
|
||||
users would expect (the more-specific `Serilog` section being the authority).
|
||||
@@ -1041,12 +1041,12 @@ but does not warn that the floor *overrides* the Serilog section's own
|
||||
|
||||
Pick one mechanism: either (a) drop the `MinimumLevel.Is` call and let
|
||||
`ReadFrom.Configuration` consume `Serilog:MinimumLevel`, migrating any docs/
|
||||
deployments that reference `ScadaLink:Logging:MinimumLevel`; or (b) keep the
|
||||
current "ScadaLink:Logging" path and reject `Serilog:MinimumLevel` if present
|
||||
deployments that reference `ScadaBridge:Logging:MinimumLevel`; or (b) keep the
|
||||
current "ScadaBridge:Logging" path and reject `Serilog:MinimumLevel` if present
|
||||
(throw at startup so the operator sees the conflict). At minimum, expand the
|
||||
XML doc + REQ-HOST-8 to spell out the precedence explicitly.
|
||||
|
||||
**Resolution (2026-05-28):** `ScadaLink:Logging:MinimumLevel` is now the documented single source of truth for the Serilog floor (Host-011's `LoggingOptions` binding), and the precedence is made visible — `LoggerConfigurationFactory.Build` writes a one-shot warning to `Console.Error` when both `ScadaLink:Logging:MinimumLevel` and `Serilog:MinimumLevel` (or `Serilog:MinimumLevel:Default`) are present, naming both values and pointing the operator at the documented key. Order of operations is unchanged — `MinimumLevel.Is(...)` deliberately runs after `ReadFrom.Configuration(...)` so the ScadaLink value wins — but the silent-override behaviour is now loud. The class XML doc gained a Host-020 paragraph explicitly spelling out the precedence. A test-visible `Build(..., TextWriter warningWriter)` overload mirrors the `ParseLevel` Host-022 pattern so the warning can be asserted in unit tests; the production four-arg overload delegates with `Console.Error`.
|
||||
**Resolution (2026-05-28):** `ScadaBridge:Logging:MinimumLevel` is now the documented single source of truth for the Serilog floor (Host-011's `LoggingOptions` binding), and the precedence is made visible — `LoggerConfigurationFactory.Build` writes a one-shot warning to `Console.Error` when both `ScadaBridge:Logging:MinimumLevel` and `Serilog:MinimumLevel` (or `Serilog:MinimumLevel:Default`) are present, naming both values and pointing the operator at the documented key. Order of operations is unchanged — `MinimumLevel.Is(...)` deliberately runs after `ReadFrom.Configuration(...)` so the ScadaBridge value wins — but the silent-override behaviour is now loud. The class XML doc gained a Host-020 paragraph explicitly spelling out the precedence. A test-visible `Build(..., TextWriter warningWriter)` overload mirrors the `ParseLevel` Host-022 pattern so the warning can be asserted in unit tests; the production four-arg overload delegates with `Console.Error`.
|
||||
|
||||
### Host-021 — Microsoft `Logging:LogLevel` section in `appsettings.json` is dead config under Serilog
|
||||
|
||||
@@ -1055,7 +1055,7 @@ XML doc + REQ-HOST-8 to spell out the precedence explicitly.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/appsettings.json:2-6` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/appsettings.json:2-6` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1075,10 +1075,10 @@ the .NET convention) sees no behaviour change — the section is dead config.
|
||||
Either remove the `Logging:LogLevel` block from `appsettings.json` (Serilog
|
||||
owns logging configuration in this Host), or replace it with a brief comment
|
||||
explaining it is intentionally retained for non-Serilog tooling. Document the
|
||||
authoritative location (`Serilog` + `ScadaLink:Logging`) in
|
||||
authoritative location (`Serilog` + `ScadaBridge:Logging`) in
|
||||
`Component-Host.md` REQ-HOST-8 if not already explicit.
|
||||
|
||||
**Resolution (2026-05-28):** Confirmed by repository-wide grep that no code reads `Logging:LogLevel` (the Host calls `builder.Host.UseSerilog()` which replaces the default `ILoggerFactory` setup with Serilog as the only provider), so the block was pure dead config. Removed the `Logging:LogLevel:Default = Information` block from `appsettings.json` and replaced it with a `_logging` doc-key comment explaining the rationale (Serilog is the sole provider) and pointing operators at the two authoritative keys: `ScadaLink:Logging:MinimumLevel` for the floor (bound to `LoggingOptions` per Host-011) and the `Serilog` section for sinks (Host-014's `ReadFrom.Configuration`). The Host-014 regression test (`SerilogSinkConfigTests.ShippedAppSettings_HasSerilogSection_WithConsoleAndFileSinks`) still asserts the surviving `Serilog` section's shape, so removing the Microsoft block did not break the existing pinning.
|
||||
**Resolution (2026-05-28):** Confirmed by repository-wide grep that no code reads `Logging:LogLevel` (the Host calls `builder.Host.UseSerilog()` which replaces the default `ILoggerFactory` setup with Serilog as the only provider), so the block was pure dead config. Removed the `Logging:LogLevel:Default = Information` block from `appsettings.json` and replaced it with a `_logging` doc-key comment explaining the rationale (Serilog is the sole provider) and pointing operators at the two authoritative keys: `ScadaBridge:Logging:MinimumLevel` for the floor (bound to `LoggingOptions` per Host-011) and the `Serilog` section for sinks (Host-014's `ReadFrom.Configuration`). The Host-014 regression test (`SerilogSinkConfigTests.ShippedAppSettings_HasSerilogSection_WithConsoleAndFileSinks`) still asserts the surviving `Serilog` section's shape, so removing the Microsoft block did not break the existing pinning.
|
||||
|
||||
### Host-022 — `ParseLevel` silently coerces unrecognised `MinimumLevel` to `Information`
|
||||
|
||||
@@ -1087,7 +1087,7 @@ authoritative location (`Serilog` + `ScadaLink:Logging`) in
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/LoggerConfigurationFactory.cs:50-55` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/LoggerConfigurationFactory.cs:50-55` |
|
||||
|
||||
**Resolution (2026-05-28):** `ParseLevel` now writes a one-shot warning to `Console.Error` (the logger isn't built yet at this point) when a non-null/non-blank `MinimumLevel` value fails to parse, naming the offending value and the `Information` fallback. Null/blank values continue to default silently (treated as "unset"). The helper gained a test-visible `TextWriter` overload so unit tests can capture the warning; the production path delegates to it with `Console.Error`. Tests `ParseLevel_UnrecognisedValue_FallsBackAndWarns`, `ParseLevel_NullOrBlank_FallsBackSilently`, and `ParseLevel_RecognisedValue_NoWarning` pin the behaviour.
|
||||
|
||||
@@ -1097,7 +1097,7 @@ authoritative location (`Serilog` + `ScadaLink:Logging`) in
|
||||
`Enum.TryParse<LogEventLevel>(level, ignoreCase: true, out var parsed)` and
|
||||
returns `LogEventLevel.Information` when parsing fails — without logging the
|
||||
fallback. An operator who sets
|
||||
`ScadaLink:Logging:MinimumLevel = "Informaiton"` (a common typo) or
|
||||
`ScadaBridge:Logging:MinimumLevel = "Informaiton"` (a common typo) or
|
||||
`"Verbose,Debug"` or any unrecognised value gets the default level silently;
|
||||
there is no warning, no log line, no startup error. Combined with Host-020
|
||||
(this is the only mechanism that pins the floor), a misspelt value is
|
||||
@@ -1111,7 +1111,7 @@ In `LoggerConfigurationFactory.Build`, when `loggingOptions.MinimumLevel` is
|
||||
non-null/non-blank but does not parse to a valid `LogEventLevel`, write a
|
||||
`Console.Error.WriteLine` warning (the logger is not yet built) and proceed
|
||||
with `Information`. Alternatively, validate the value in `StartupValidator`
|
||||
and fail fast — that matches the pattern used for other ScadaLink
|
||||
and fail fast — that matches the pattern used for other ScadaBridge
|
||||
configuration keys. Add a `LoggerConfigurationTests` case asserting the
|
||||
behaviour you choose.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.InboundAPI` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.InboundAPI` |
|
||||
| Design doc | `docs/requirements/Component-InboundAPI.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -33,7 +33,7 @@ are High severity and should be addressed before production use.
|
||||
#### Re-review 2026-05-17 (commit `39d737e`)
|
||||
|
||||
All 13 findings from the initial review remain `Resolved`; the module source under
|
||||
`src/ScadaLink.InboundAPI` is unchanged since the last InboundAPI fix commit
|
||||
`src/ZB.MOM.WW.ScadaBridge.InboundAPI` is unchanged since the last InboundAPI fix commit
|
||||
(`8dd7412`), which precedes `39d737e`. This re-review re-walked all 10 checklist
|
||||
categories against the resolved code and surfaced **4 new findings** — none touching
|
||||
the previously-fixed concurrency/trust-model code, but all in areas the first pass
|
||||
@@ -68,16 +68,16 @@ statement that the timeout covers routed calls (InboundAPI-016); and (4) `RouteH
|
||||
|
||||
All 17 prior findings remain `Resolved`. The module has grown materially since the
|
||||
last pass — a new `AuditWriteMiddleware` (Audit Log #23 M4 Bundle D) now lives under
|
||||
`src/ScadaLink.InboundAPI/Middleware/`, the `ApiKeyValidator` was rewired to hash the
|
||||
`src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/`, the `ApiKeyValidator` was rewired to hash the
|
||||
candidate with `IApiKeyHasher` (ConfigurationDatabase-012), and an `IInstanceRouter`
|
||||
seam was introduced. This re-review re-walked all 10 checklist categories against
|
||||
`1eb6e97` and surfaced **8 new findings** concentrated on the new audit middleware
|
||||
and a stranded follow-up from InboundAPI-008:
|
||||
|
||||
1. The InboundAPI-008 resolution explicitly deferred registering an `IActiveNodeGate`
|
||||
implementation in `ScadaLink.Host` as a "follow-up outside this module's scope" —
|
||||
implementation in `ZB.MOM.WW.ScadaBridge.Host` as a "follow-up outside this module's scope" —
|
||||
that follow-up is still unfulfilled (no production registration anywhere in
|
||||
`src/ScadaLink.Host/`), so the design-mandated standby-node gating is silently
|
||||
`src/ZB.MOM.WW.ScadaBridge.Host/`), so the design-mandated standby-node gating is silently
|
||||
disabled in production today (`InboundAPI-022`, High).
|
||||
2. `AuditWriteMiddleware` is wired in `Program.cs` against `/api/*` rather than the
|
||||
specific `POST /api/{methodName}` route, so GETs against `/api/audit/query` and
|
||||
@@ -133,7 +133,7 @@ configuration database, but the invariant is undocumented.)
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:17`, `:32`, `:40`, `:89`, `:123-128` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:17`, `:32`, `:40`, `:89`, `:123-128` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -169,7 +169,7 @@ via `GetOrAdd` so concurrent first-callers share one handler. Regression tests
|
||||
| Severity | Medium — re-triaged: already fixed by the InboundAPI-001 fix; verified and closed |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:152-161` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:152-161` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -209,7 +209,7 @@ handler that `GetOrAdd` keeps. Regression test
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/InboundApiRepository.cs:22-23`, consumed by `src/ScadaLink.InboundAPI/ApiKeyValidator.cs:33` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/InboundApiRepository.cs:22-23`, consumed by `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiKeyValidator.cs:33` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -251,7 +251,7 @@ longer depends on it.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:117-141` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:117-141` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -293,7 +293,7 @@ added.
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:56-93` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:56-93` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -337,7 +337,7 @@ Regression tests `CompileAndRegister_ForbiddenApi_RejectsScript` (theory),
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/EndpointExtensions.cs:54-62` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs:54-62` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -365,7 +365,7 @@ It rejects requests whose declared `Content-Length` exceeds `InboundApiOptions.
|
||||
MaxRequestBodyBytes` (default 1 MiB) with HTTP 413 *before* the handler buffers the
|
||||
body into a `JsonDocument`, and also lowers the per-request `IHttpMaxRequestBodySizeFeature`
|
||||
cap so a chunked/unknown-length stream is cut off by Kestrel while being read. The
|
||||
limit is configurable via the bound `ScadaLink:InboundApi` options section. Regression
|
||||
limit is configurable via the bound `ScadaBridge:InboundApi` options section. Regression
|
||||
tests `OversizedBody_ShortCircuitsWith413_AndDoesNotRunHandler`, `BodyAtLimit_RunsHandler`,
|
||||
and `FilterCapsMaxRequestBodySizeFeature` added.
|
||||
|
||||
@@ -376,7 +376,7 @@ and `FilterCapsMaxRequestBodySizeFeature` added.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:188-203` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:188-203` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -402,7 +402,7 @@ Resolved 2026-05-16 (commit `<pending>`). The drift was confirmed real:
|
||||
so a method script following the documented `Database.Connection("name")` API
|
||||
would fail to compile. Resolution direction: the design doc is stale, not the
|
||||
code. Implementing `Database.Connection()` would hand inbound API scripts a
|
||||
*raw* MS SQL client, in direct tension with the ScadaLink script trust model
|
||||
*raw* MS SQL client, in direct tension with the ScadaBridge script trust model
|
||||
(scripts are forbidden `System.IO`, raw network, etc.; `ForbiddenApiChecker`
|
||||
statically enforces this). Rather than carve a hole in the trust model, the
|
||||
"Database Access" section was removed from `docs/requirements/Component-InboundAPI.md`
|
||||
@@ -418,7 +418,7 @@ explicit design change. Code and doc now agree; no code or test change required.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/EndpointExtensions.cs:19-23`, `src/ScadaLink.Host/Program.cs:149` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs:19-23`, `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:149` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -447,7 +447,7 @@ auth/script work, so Traefik/clients only reach the live node — consistent wit
|
||||
registered (non-clustered host / tests) the endpoint defaults to "allow", preserving
|
||||
prior behaviour. Regression tests `StandbyNode_ShortCircuitsWith503_AndDoesNotRunHandler`,
|
||||
`ActiveNode_PassesGate_RunsHandler`, and `NoGateRegistered_PassesGate_RunsHandler`
|
||||
added. **Follow-up (outside this module's scope):** `ScadaLink.Host` should register
|
||||
added. **Follow-up (outside this module's scope):** `ZB.MOM.WW.ScadaBridge.Host` should register
|
||||
an `IActiveNodeGate` implementation backed by `ActiveNodeHealthCheck` /
|
||||
`Cluster.State.Leader` in the central-role branch of `Program.cs` so the gate is
|
||||
actually enforced in production; until then the endpoint defaults to "allow".
|
||||
@@ -459,7 +459,7 @@ actually enforced in production; until then the endpoint defaults to "allow".
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:123-128` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:123-128` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -496,7 +496,7 @@ re-evaluated. Regression tests `FailedCompilation_IsNotRetriedOnEveryRequest`
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/ParameterValidator.cs:64-90`, `:112-118` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ParameterValidator.cs:64-90`, `:112-118` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -541,7 +541,7 @@ follow-up. Regression tests `UnexpectedBodyField_ReturnsInvalid` and
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/ApiKeyValidator.cs:39-52` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiKeyValidator.cs:39-52` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -580,14 +580,14 @@ indistinguishable contract.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/ParameterValidator.cs:128-133` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ParameterValidator.cs:128-133` |
|
||||
|
||||
**Description**
|
||||
|
||||
`ParameterDefinition` is a persistence-/contract-shaped POCO: it is the deserialized
|
||||
form of `ApiMethod.ParameterDefinitions` (a column in the configuration database) and
|
||||
describes the public API contract. CLAUDE.md's code-organization rules place
|
||||
persistence-ignorant entity/contract types in `ScadaLink.Commons`. Defining it inside
|
||||
persistence-ignorant entity/contract types in `ZB.MOM.WW.ScadaBridge.Commons`. Defining it inside
|
||||
the InboundAPI project means any other component that needs to read or produce method
|
||||
parameter definitions (e.g. Central UI's method editor, CLI, Management Service)
|
||||
cannot share the type and will duplicate it.
|
||||
@@ -595,7 +595,7 @@ cannot share the type and will duplicate it.
|
||||
**Recommendation**
|
||||
|
||||
Move `ParameterDefinition` (and a matching return-definition type, if added) to
|
||||
`ScadaLink.Commons` under the InboundApi entity/types namespace so it is shared by all
|
||||
`ZB.MOM.WW.ScadaBridge.Commons` under the InboundApi entity/types namespace so it is shared by all
|
||||
components that work with method definitions.
|
||||
|
||||
**Resolution**
|
||||
@@ -604,22 +604,22 @@ Resolved 2026-05-16 (commit `<pending>`): root cause confirmed against the sourc
|
||||
`ParameterDefinition` was a persistence-ignorant, API-contract-shaped POCO (the
|
||||
deserialized form of the `ApiMethod.ParameterDefinitions` configuration-database
|
||||
column) declared inside the component project, contrary to CLAUDE.md's
|
||||
code-organization rule that such shared contract types live in `ScadaLink.Commons`.
|
||||
The type was moved to `src/ScadaLink.Commons/Types/InboundApi/ParameterDefinition.cs`
|
||||
(namespace `ScadaLink.Commons.Types.InboundApi`) — placed under `Types/` with an
|
||||
code-organization rule that such shared contract types live in `ZB.MOM.WW.ScadaBridge.Commons`.
|
||||
The type was moved to `src/ZB.MOM.WW.ScadaBridge.Commons/Types/InboundApi/ParameterDefinition.cs`
|
||||
(namespace `ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi`) — placed under `Types/` with an
|
||||
`InboundApi` domain subfolder, matching the existing `Types/Scripts/` precedent, since
|
||||
the column itself is the persisted form and this type is its deserialized contract
|
||||
shape (not an EF-mapped entity). It remains a pure POCO with no EF attributes and no
|
||||
behaviour. `ParameterValidator` now imports the moved type via a `using
|
||||
ScadaLink.Commons.Types.InboundApi;` directive; a tree-wide search confirmed
|
||||
ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;` directive; a tree-wide search confirmed
|
||||
`ParameterValidator.cs` was the type's only declaration and only direct consumer (all
|
||||
other `ParameterDefinition*` matches are the unrelated `ParameterDefinitions` string
|
||||
property). No return-definition type exists in the codebase — only a `ReturnDefinition`
|
||||
string column — so none was invented. No behavioural change, so no new runtime
|
||||
regression test: this is a compile-level type move, and the existing 52
|
||||
`ScadaLink.InboundAPI.Tests` (including the `ParameterValidator` suite) act as the
|
||||
regression guard. `dotnet test` for `ScadaLink.InboundAPI.Tests` (52 passed) and
|
||||
`ScadaLink.Commons.Tests` (226 passed) are green; `dotnet build ScadaLink.slnx`
|
||||
`ZB.MOM.WW.ScadaBridge.InboundAPI.Tests` (including the `ParameterValidator` suite) act as the
|
||||
regression guard. `dotnet test` for `ZB.MOM.WW.ScadaBridge.InboundAPI.Tests` (52 passed) and
|
||||
`ZB.MOM.WW.ScadaBridge.Commons.Tests` (226 passed) are green; `dotnet build ZB.MOM.WW.ScadaBridge.slnx`
|
||||
succeeds with 0 warnings / 0 errors.
|
||||
|
||||
### InboundAPI-013 — `ApiKeyValidationResult.NotFound` factory returns HTTP 400, contradicting its name
|
||||
@@ -629,7 +629,7 @@ succeeds with 0 warnings / 0 errors.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/ApiKeyValidator.cs:78-79` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiKeyValidator.cs:78-79` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -667,7 +667,7 @@ from "key not approved"), but that doc edit is outside this module's editable sc
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:201-205`, `src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs:10` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:201-205`, `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/InboundApi/ApiMethod.cs:10` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -725,7 +725,7 @@ is validation-only (no coercion). Regression tests: `ReturnValueValidatorTests`
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/ForbiddenApiChecker.cs:63-119`, `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:109-126` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ForbiddenApiChecker.cs:63-119`, `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:109-126` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -799,7 +799,7 @@ namespace-deny-list regression guards.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/RouteHelper.cs:59-152`, `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:177`, `:199` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs:59-152`, `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:177`, `:199` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -863,13 +863,13 @@ of running orphaned. Regression tests (in the new `RouteHelperTests`):
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/RouteHelper.cs:1-165`, `tests/ScadaLink.InboundAPI.Tests/` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs:1-165`, `tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/` |
|
||||
|
||||
**Description**
|
||||
|
||||
`RouteHelper`/`RouteTarget` is the entire WP-4 cross-site routing surface — the
|
||||
`Route.To().Call()/GetAttribute(s)/SetAttribute(s)` API that inbound API scripts use
|
||||
to reach instances at any site. It has zero tests: the `ScadaLink.InboundAPI.Tests`
|
||||
to reach instances at any site. It has zero tests: the `ZB.MOM.WW.ScadaBridge.InboundAPI.Tests`
|
||||
project covers `ApiKeyValidator`, `ParameterValidator`, `InboundScriptExecutor`, and
|
||||
`InboundApiEndpointFilter`, but no test file exercises `RouteHelper`. Untested
|
||||
behaviours include site resolution via `IInstanceLocator` (including the
|
||||
@@ -892,7 +892,7 @@ wiring is added.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-17 (commit `<pending>`): confirmed — `ScadaLink.InboundAPI.Tests` had
|
||||
Resolved 2026-05-17 (commit `<pending>`): confirmed — `ZB.MOM.WW.ScadaBridge.InboundAPI.Tests` had
|
||||
no file exercising `RouteHelper`/`RouteTarget`. To make the surface testable without a
|
||||
live actor system, an `IInstanceRouter` seam was introduced in the module (the routing
|
||||
transport `RouteHelper` depends on); the production `CommunicationServiceInstanceRouter`
|
||||
@@ -912,7 +912,7 @@ the InboundAPI-016 deadline-token inheritance behaviour. All 15 pass.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs:257` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs:257` |
|
||||
|
||||
**Resolution (2026-05-28):** kept the fire-and-forget (audit emission must
|
||||
never block or alter the user-facing response per alog.md §13) but added
|
||||
@@ -968,7 +968,7 @@ synchronous throw) to pin the new contract.
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Location | `src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs:141` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs:141` |
|
||||
| Status | Resolved |
|
||||
|
||||
**Resolution (2026-05-28):** Added a `RequestHasBody(HttpRequest)` guard that returns `true` only when `ContentLength > 0` or the method is POST / PUT / PATCH (the `HttpMethods.IsPost/Put/Patch` helpers); the `EnableBuffering` + `ReadBufferedRequestBodyAsync` call now sits behind that guard. Bodyless GET / HEAD / DELETE / TRACE / OPTIONS requests (and any explicit `Content-Length: 0`) skip the `FileBufferingReadStream` allocation; body-carrying methods with no Content-Length (chunked POST etc.) still buffer. Regression tests `BodylessMethod_SkipsEnableBuffering_RequestStreamIsNotReplaced` (GET/HEAD/DELETE theory), `BodylessPost_ContentLengthZero_SkipsEnableBuffering`, and `PostWithBody_StillEnablesBuffering_AndCapturesRequestSummary` (anti-regression).
|
||||
@@ -1000,7 +1000,7 @@ bodyless request through the middleware.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/EndpointExtensions.cs:70` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs:70` |
|
||||
|
||||
**Resolution (2026-05-28):** swapped the case-sensitive `Contains("json")`
|
||||
substring match for `Contains("json", StringComparison.OrdinalIgnoreCase)` so
|
||||
@@ -1041,7 +1041,7 @@ regression test posting with `application/JSON` and Transfer-Encoding: chunked.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/RouteHelper.cs:141-143`, `:182-183`, `:225-226`; `src/ScadaLink.Commons/Messages/InboundApi/RouteToInstanceRequest.cs:15-21`, `:36-40`, `:55-59` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs:141-143`, `:182-183`, `:225-226`; `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/InboundApi/RouteToInstanceRequest.cs:15-21`, `:36-40`, `:55-59` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1104,9 +1104,9 @@ backlog), they can stamp the parent id without any further plumbing.
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/IActiveNodeGate.cs`, `src/ScadaLink.InboundAPI/InboundApiEndpointFilter.cs:52-60`; absent from `src/ScadaLink.Host/Program.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/IActiveNodeGate.cs`, `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundApiEndpointFilter.cs:52-60`; absent from `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs` |
|
||||
|
||||
**Resolution** — Added `src/ScadaLink.Host/Health/ActiveNodeGate.cs`, a production `IActiveNodeGate` implementation backed by `AkkaHostedService` that mirrors `ActiveNodeHealthCheck`'s leadership probe (member status `Up` AND `Cluster.State.Leader == SelfAddress`), and registered it as a singleton in the central-role branch of `Program.cs`. A structural regression test (`CentralCompositionRootTests.Central_IActiveNodeGate_IsRegisteredAsActiveNodeGate`) reflects over the built `IServiceProvider` to assert the registration's existence and concrete type — failing on `main` and passing after the fix. The `InboundApiEndpointFilter`'s fall-through-to-allow behaviour is retained as the documented safe default for non-clustered hosts and tests.
|
||||
**Resolution** — Added `src/ZB.MOM.WW.ScadaBridge.Host/Health/ActiveNodeGate.cs`, a production `IActiveNodeGate` implementation backed by `AkkaHostedService` that mirrors `ActiveNodeHealthCheck`'s leadership probe (member status `Up` AND `Cluster.State.Leader == SelfAddress`), and registered it as a singleton in the central-role branch of `Program.cs`. A structural regression test (`CentralCompositionRootTests.Central_IActiveNodeGate_IsRegisteredAsActiveNodeGate`) reflects over the built `IServiceProvider` to assert the registration's existence and concrete type — failing on `main` and passing after the fix. The `InboundApiEndpointFilter`'s fall-through-to-allow behaviour is retained as the documented safe default for non-clustered hosts and tests.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1118,14 +1118,14 @@ when `gate is { IsActiveNode: false }` returns HTTP 503. The filter's behaviour
|
||||
when **no implementation is registered** (line 51 comment) is to fall through and
|
||||
serve the request — the resolution paragraph for InboundAPI-008 closes with:
|
||||
|
||||
> "Follow-up (outside this module's scope): `ScadaLink.Host` should register an
|
||||
> "Follow-up (outside this module's scope): `ZB.MOM.WW.ScadaBridge.Host` should register an
|
||||
> `IActiveNodeGate` implementation backed by `ActiveNodeHealthCheck` /
|
||||
> `Cluster.State.Leader` in the central-role branch of `Program.cs` so the gate is
|
||||
> actually enforced in production; until then the endpoint defaults to "allow"."
|
||||
|
||||
A grep of the entire `src/ScadaLink.Host/` tree at `1eb6e97` finds **zero**
|
||||
A grep of the entire `src/ZB.MOM.WW.ScadaBridge.Host/` tree at `1eb6e97` finds **zero**
|
||||
`IActiveNodeGate` registrations: `grep -rn "IActiveNodeGate\|AddSingleton.*ActiveNode"
|
||||
src/ScadaLink.Host/` returns no matches. The follow-up was never carried out. So
|
||||
src/ZB.MOM.WW.ScadaBridge.Host/` returns no matches. The follow-up was never carried out. So
|
||||
in production today the standby central node still serves the inbound API exactly
|
||||
as InboundAPI-008 described — executes method scripts, runs `Route.To()` calls,
|
||||
races the active node, and may operate against stale singleton state. The new
|
||||
@@ -1138,7 +1138,7 @@ The design says the inbound API is "Central cluster only (active node)" and
|
||||
**Recommendation**
|
||||
|
||||
Register an `IActiveNodeGate` implementation in the central-role branch of
|
||||
`ScadaLink.Host/Program.cs`. The natural backing is the existing
|
||||
`ZB.MOM.WW.ScadaBridge.Host/Program.cs`. The natural backing is the existing
|
||||
`ActiveNodeHealthCheck` (already wired for `/health/active`) or a direct read of
|
||||
`Cluster.Get(actorSystem).State.Leader == Cluster.Get(actorSystem).SelfAddress`.
|
||||
Add an integration test in the Host that spins up the central role and asserts
|
||||
@@ -1153,9 +1153,9 @@ realisation of the InboundAPI-008 vulnerability.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/EndpointExtensions.cs:31-140`, `tests/ScadaLink.InboundAPI.Tests/` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs:31-140`, `tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `tests/ScadaLink.InboundAPI.Tests/EndpointExtensionsTests.cs`, a `TestServer`-hosted suite (same pattern as `EndpointContentTypeTests`) that drives the `POST /api/{methodName}` wiring end-to-end. Seven cases pin the composed flow: happy path (200 + script result body), missing API key (401), unknown method (403, indistinguishable from "not approved" per InboundAPI-011), invalid JSON body (400), missing required parameter (400 from `ParameterValidator`), script throws (500 with sanitized error body — the executor's catch-all replaces the raw exception with `"Internal script error"`), and the `HttpContext.Items[AuditWriteMiddleware.AuditActorItemKey]` actor-stash invariant (verified by an inline capture middleware reading the slot after the endpoint runs). All 7 new tests pass; total InboundAPI.Tests now 158 (was 151).
|
||||
**Resolution (2026-05-28):** Added `tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/EndpointExtensionsTests.cs`, a `TestServer`-hosted suite (same pattern as `EndpointContentTypeTests`) that drives the `POST /api/{methodName}` wiring end-to-end. Seven cases pin the composed flow: happy path (200 + script result body), missing API key (401), unknown method (403, indistinguishable from "not approved" per InboundAPI-011), invalid JSON body (400), missing required parameter (400 from `ParameterValidator`), script throws (500 with sanitized error body — the executor's catch-all replaces the raw exception with `"Internal script error"`), and the `HttpContext.Items[AuditWriteMiddleware.AuditActorItemKey]` actor-stash invariant (verified by an inline capture middleware reading the slot after the endpoint runs). All 7 new tests pass; total InboundAPI.Tests now 158 (was 151).
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1190,7 +1190,7 @@ resolved key name after successful auth, but is absent on auth failures).
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:30`, `:77`, `:223`, `:233` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:30`, `:77`, `:223`, `:233` |
|
||||
|
||||
**Resolution (2026-05-28):** capped the cache at `KnownBadMethodsCap = 1000`
|
||||
entries via a new `TryRecordBadMethod` helper that short-circuits when the cap
|
||||
@@ -1236,7 +1236,7 @@ immediate change required; this is a watch-list item.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Program.cs:183-185`; consumers: `src/ScadaLink.ManagementService/AuditEndpoints.cs:93-94`; emitter: `src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs:175-252` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:183-185`; consumers: `src/ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs:93-94`; emitter: `src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs:175-252` |
|
||||
|
||||
**Resolution (2026-05-28):** Took the defensive path-exclusion in
|
||||
`Program.cs` (Option 1 from the recommendation). The `UseWhen` predicate
|
||||
@@ -1255,7 +1255,7 @@ the excluded prefixes, which would be noisy in code review.
|
||||
`Program.cs` wires the audit middleware as
|
||||
`app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), branch => branch.UseAuditWriteMiddleware())`
|
||||
— scoped to the `/api` *prefix*, not to the `POST /api/{methodName}` route.
|
||||
Meanwhile, `ScadaLink.ManagementService/AuditEndpoints.cs` maps
|
||||
Meanwhile, `ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs` maps
|
||||
`MapGet("/api/audit/query", ...)` (line 93) and `MapGet("/api/audit/export", ...)`
|
||||
(line 94). Both routes therefore inherit `AuditWriteMiddleware`, which emits an
|
||||
`AuditEvent { Channel = AuditChannel.ApiInbound, Kind = AuditKind.InboundRequest, ... }`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.ManagementService` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.ManagementService` |
|
||||
| Design doc | `docs/requirements/Component-ManagementService.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -31,7 +31,7 @@ authorization bypass with no workaround.
|
||||
#### Re-review 2026-05-17 (commit `39d737e`)
|
||||
|
||||
All thirteen prior findings remain correctly closed; the source under
|
||||
`src/ScadaLink.ManagementService` is byte-identical between the previously reviewed state
|
||||
`src/ZB.MOM.WW.ScadaBridge.ManagementService` is byte-identical between the previously reviewed state
|
||||
and `39d737e` (the resolution commits of findings 001–013 are folded into the history at or
|
||||
before `39d737e`). ManagementService-012 was re-checked and its **Deferred** status still
|
||||
holds: `ManagementEnvelope.Command` is still typed `object`, and the marker-interface fix
|
||||
@@ -111,7 +111,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:1465`, `:1481`, `:1493`, `:641`, `:649` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:1465`, `:1481`, `:1493`, `:641`, `:649` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -153,7 +153,7 @@ Resolved 2026-05-16 (commit `<pending>`). Threaded `AuthenticatedUser` into
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:510`, `:673`, `:733`, `:774`, `:631`, `:624` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:510`, `:673`, `:733`, `:774`, `:631`, `:624` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -192,7 +192,7 @@ the resolved entity's site ID (instance `SiteId`, site `Id`, data-connection `Si
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/DebugStreamHub.cs:104` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/DebugStreamHub.cs:104` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -231,7 +231,7 @@ tests: `IsInstanceAccessAllowed_SiteScopedUser_OutOfScopeInstance_Denied`,
|
||||
| Severity | Medium |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:61` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:61` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -271,7 +271,7 @@ mapping tests confirm behaviour is preserved.
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:33` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:33` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -306,7 +306,7 @@ children today, so this is a forward-looking correctness fix. Regression tests:
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementEndpoints.cs:83`, `:112` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs:83`, `:112` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -342,7 +342,7 @@ string rather than parsing a throwaway document. Regression tests:
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:67`; `src/ScadaLink.ManagementService/ManagementEndpoints.cs:113` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:67`; `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs:113` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -380,7 +380,7 @@ Regression tests: `SerializeResult_WithCyclicGraph_DoesNotThrow`,
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:285` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:285` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -403,7 +403,7 @@ Resolved 2026-05-16 (commit pending). Confirmed: `HandleResolveRoles` did
|
||||
(the two-step ResolveRoles flow is retired). Resolving 011 by deleting the
|
||||
`ResolveRolesCommand` dispatch case and `HandleResolveRoles` handler also removes the only
|
||||
manually-constructed `RoleMapper` in the module, so the DI-bypass no longer exists. No
|
||||
remaining `new RoleMapper` in `src/ScadaLink.ManagementService`. Regression covered by
|
||||
remaining `new RoleMapper` in `src/ZB.MOM.WW.ScadaBridge.ManagementService`. Regression covered by
|
||||
`ResolveRolesCommand_IsNoLongerDispatched_ReturnsManagementError`.
|
||||
|
||||
### ManagementService-009 — Audit logging applied inconsistently across mutating handlers
|
||||
@@ -413,7 +413,7 @@ remaining `new RoleMapper` in `src/ScadaLink.ManagementService`. Regression cove
|
||||
| Severity | Low — re-triaged from Medium; the claimed audit gap does not exist (see Description), leaving only an undocumented-convention issue. |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:357`, `:1134`, `:1085`, `:526`, `:1275` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:357`, `:1134`, `:1085`, `:526`, `:1275` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -461,7 +461,7 @@ covers the explicit-audit path.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementServiceOptions.cs:5`; `src/ScadaLink.ManagementService/ManagementEndpoints.cs:16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementServiceOptions.cs:5`; `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs:16` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -497,7 +497,7 @@ Regression tests: `ResolveAskTimeout_UsesConfiguredCommandTimeout`,
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:273`, `:283` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:273`, `:283` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -526,7 +526,7 @@ falls through to the `NotSupportedException` default and gets a uniform `Managem
|
||||
(closing the unauthenticated role-mapping enumeration surface, since `GetRequiredRole`
|
||||
returned null for it). A code comment at the former dispatch site documents the intentional
|
||||
omission. Note: the `ResolveRolesCommand` *record* itself lives in
|
||||
`src/ScadaLink.Commons/Messages/Management/SecurityCommands.cs` and was left in place — that
|
||||
`src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/SecurityCommands.cs` and was left in place — that
|
||||
file is outside this module's permitted edit scope; deleting the orphan record should be done
|
||||
as a Commons-module follow-up. With the handler removed it is now an inert,
|
||||
registry-only type with no behaviour. Regression test:
|
||||
@@ -539,7 +539,7 @@ registry-only type with no behaviour. Regression test:
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Deferred |
|
||||
| Location | `src/ScadaLink.Commons/Messages/Management/ManagementEnvelope.cs:7`; `src/ScadaLink.ManagementService/ManagementActor.cs:132` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/ManagementEnvelope.cs:7`; `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:132` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -561,8 +561,8 @@ flag unhandled cases, and keeps `ManagementCommandRegistry`'s reflection scan pr
|
||||
Deferred 2026-05-16. Finding verified as genuine: `ManagementEnvelope.Command` is typed
|
||||
`object` and the recommended `IManagementCommand` marker-interface fix is sound. However, the
|
||||
fix cannot be implemented within the `ManagementService` module: both `ManagementEnvelope` and
|
||||
all ~50 `*Command` records live in `src/ScadaLink.Commons/Messages/Management/` (17 files),
|
||||
which is outside this work item's permitted edit scope (`src/ScadaLink.ManagementService/**`,
|
||||
all ~50 `*Command` records live in `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/` (17 files),
|
||||
which is outside this work item's permitted edit scope (`src/ZB.MOM.WW.ScadaBridge.ManagementService/**`,
|
||||
its tests, and this findings file only). Adding the marker interface, retyping the envelope,
|
||||
and having `ManagementCommandRegistry` constrain its reflection scan to `IManagementCommand`
|
||||
implementers is a cohesive Commons-module change and must be done there — also so the Commons
|
||||
@@ -576,7 +576,7 @@ work item; no `ManagementService`-local change is appropriate.
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs:1` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs:1` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -618,7 +618,7 @@ pre-existing site-scope and DebugStreamHub suites. `dotnet test` -> 48 passed.
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:306`, `:1174` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:306`, `:1174` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -672,7 +672,7 @@ Regression tests: `QueryDeployments_WithDesignRole_ReturnsUnauthorized`,
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:647`–`:659` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:647`–`:659` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -716,7 +716,7 @@ this residual is documented in a code comment on the handler. Regression tests:
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:121`–`:131` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:121`–`:131` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -761,7 +761,7 @@ leaked.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs:1` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs:1` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -797,7 +797,7 @@ Deployment user and an Admin user, in- and out-of-scope
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:153`–`:207`, `:336`, `:1302` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:153`–`:207`, `:336`, `:1302` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -858,7 +858,7 @@ pre-fix code (the command fell through to "any authenticated user") and pass aft
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/AuditEndpoints.cs:358`–`:368`, `:397`–`:437` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs:358`–`:368`, `:397`–`:437` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -928,7 +928,7 @@ mixed-set intersected.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:1136`–`:1153` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:1136`–`:1153` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `SmtpConfigPublicShape` projection that
|
||||
returns every non-secret field plus a `HasCredentials` bool — never the
|
||||
@@ -989,7 +989,7 @@ Add regression tests: `UpdateSmtpConfig_DoesNotEchoCredentialsInResponse` and
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs:1`; `src/ScadaLink.ManagementService/ManagementActor.cs:1717`–`:1897` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs:1`; `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:1717`–`:1897` |
|
||||
|
||||
**Resolution (2026-05-28):** Added six `ManagementActorTests` cases covering the load-bearing bundle behaviours. Role gating: `ExportBundleCommand_WithAdminRole_ReturnsUnauthorized` (Export needs Design), `PreviewBundleCommand_WithDesignRole_ReturnsUnauthorized`, and `ImportBundleCommand_WithDesignRole_ReturnsUnauthorized` (Preview/Import need Admin). Name resolution: `ExportBundleCommand_WithUnknownTemplateName_ReturnsManagementError` proves the `ResolveIds` `ManagementCommandException` surfaces verbatim with the missing entity type + name. Handler logic: `ImportBundleCommand_WithBlockerRow_AbortsBeforeApply` seeds a `ConflictKind.Blocker` preview and asserts `IBundleImporter.ApplyAsync` is never called and the error names the blocker; `ImportBundleCommand_DuplicatePreviewItems_DedupePerEntityTypeAndName` seeds three rows for the same `(Template, "Dup")` key (Identical, Modified, Identical) and asserts only one resolution reaches `ApplyAsync` with last-write-wins (`Skip`, overriding the prior `Modified`/`Overwrite`). All bundle tests use substituted `IBundleExporter`/`IBundleImporter` via a new `AddBundleSubstitutes()` helper plus stub `GetAll*Async` repository returns. 106/106 ManagementService.Tests pass.
|
||||
|
||||
@@ -1095,7 +1095,7 @@ Update `Component-ManagementService.md`:
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.ManagementService/ManagementActor.cs:1276`–`:1295` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs:1276`–`:1295` |
|
||||
|
||||
**Resolution (2026-05-28):** Swapped the per-`InstanceId` `GetInstanceByIdAsync` lookup loop for a single `templateRepo.GetAllInstancesAsync()` bulk fetch, projected into a `Dictionary<int, int?>` keyed by `Instance.Id`. The unfiltered branch now hits the configuration database exactly twice (deployment records + instances) regardless of fleet size. Existing test `QueryDeployments_UnfilteredForSiteScopedUser_DropsOutOfScopeRecords` was updated to mock the bulk path and to assert `GetInstanceByIdAsync` is no longer used on the unfiltered branch; new regression test `QueryDeployments_UnfilteredForSiteScopedUser_UsesBulkInstanceLoad_NotPerRecordLookup` pins `GetAllInstancesAsync` is invoked exactly once even with duplicate-instance records.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.NotificationOutbox` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox` |
|
||||
| Design doc | `docs/requirements/Component-NotificationOutbox.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -28,7 +28,7 @@ ConfigurationDatabase**. The outbox inherits two known defects from its sibling
|
||||
that are reachable through `EmailNotificationDeliveryAdapter`: the OAuth2 SASL empty-user
|
||||
bug (NS-021) ships every M365 send with `user=""`, and the
|
||||
`InsertIfNotExistsAsync` check-then-act race (CD-015) lives on the outbox's ack-after-persist
|
||||
hot path. Neither is a defect of code under `src/ScadaLink.NotificationOutbox/`, but both
|
||||
hot path. Neither is a defect of code under `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/`, but both
|
||||
are surfaced here because production dispatch and ingest go through these exact lines.
|
||||
A secondary theme is **dispatcher-fire-and-forget audit writes** (`_ = _auditWriter.WriteAsync(...)`)
|
||||
that can race the per-sweep scope dispose under the wrong DI graph, and a few smaller
|
||||
@@ -63,7 +63,7 @@ from `Component-NotificationOutbox.md`. No Critical findings; two High, six Medi
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/Delivery/EmailNotificationDeliveryAdapter.cs:185-191` (calls `smtp.AuthenticateAsync("oauth2", token)`); root cause in `src/ScadaLink.NotificationService/MailKitSmtpClientWrapper.cs:76-79` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/EmailNotificationDeliveryAdapter.cs:185-191` (calls `smtp.AuthenticateAsync("oauth2", token)`); root cause in `src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs:76-79` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -106,7 +106,7 @@ _Unresolved._
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs:348-360` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs:348-360` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -146,7 +146,7 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs:334`, `src/ScadaLink.NotificationOutbox/Delivery/INotificationDeliveryAdapter.cs:22` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs:334`, `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/INotificationDeliveryAdapter.cs:22` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -186,14 +186,14 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs:425-435`, `463-485` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs:425-435`, `463-485` |
|
||||
|
||||
**Description**
|
||||
|
||||
Both emission helpers issue `_ = _auditWriter.WriteAsync(evt);` — discarding the
|
||||
returned task. `CentralAuditWriter.WriteAsync` opens its own `await using var scope =
|
||||
_services.CreateAsyncScope();` and resolves a scoped `IAuditLogRepository` (verified
|
||||
at `src/ScadaLink.AuditLog/Central/CentralAuditWriter.cs:118-121`), so the writer is
|
||||
at `src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/CentralAuditWriter.cs:118-121`), so the writer is
|
||||
defensively scope-independent. However the dispatcher already holds a per-sweep
|
||||
`using var scope = _serviceProvider.CreateScope();` and the per-notification
|
||||
`UpdateAsync` runs in that scope. The fire-and-forget pattern means:
|
||||
@@ -238,15 +238,15 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs:127-132` (caller); root cause in `src/ScadaLink.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs:33-45` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs:127-132` (caller); root cause in `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs:33-45` |
|
||||
|
||||
**Resolution (2026-05-28):** Closed by CD-015 — `NotificationOutboxRepository.InsertIfNotExistsAsync` (commit `ac96b83`) is now a single-statement `IF NOT EXISTS ... INSERT` via `ExecuteSqlInterpolatedAsync` with a `SqlException` filter swallowing duplicate-key violations (`2601`/`2627`) as a no-op (`return false`). The check-then-act window is eliminated; the at-least-once handoff contract holds and the actor's `PipeTo` success/failure projection no longer surfaces a permanent PK-violation back to the site. Verified in `src/ScadaLink.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs:51-103`.
|
||||
**Resolution (2026-05-28):** Closed by CD-015 — `NotificationOutboxRepository.InsertIfNotExistsAsync` (commit `ac96b83`) is now a single-statement `IF NOT EXISTS ... INSERT` via `ExecuteSqlInterpolatedAsync` with a `SqlException` filter swallowing duplicate-key violations (`2601`/`2627`) as a no-op (`return false`). The check-then-act window is eliminated; the at-least-once handoff contract holds and the actor's `PipeTo` success/failure projection no longer surfaces a permanent PK-violation back to the site. Verified in `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs:51-103`.
|
||||
|
||||
**Description**
|
||||
|
||||
`HandleSubmit` → `PersistAsync` calls `repository.InsertIfNotExistsAsync(notification)`
|
||||
on `INotificationOutboxRepository`. The current implementation
|
||||
(`src/ScadaLink.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs`)
|
||||
(`src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs`)
|
||||
does a check-then-act with no duplicate-key catch — documented as CD-015 (High,
|
||||
Open). The Notification Outbox's documented contract is "at-least-once handoff with
|
||||
ack-after-persist plus insert-if-not-exists on `NotificationId`" (CLAUDE.md,
|
||||
@@ -280,7 +280,7 @@ remains the CD-015 raw-SQL `IF NOT EXISTS … INSERT` with `2601/2627` catch in
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs:267-277` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs:267-277` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -319,7 +319,7 @@ stateful intent (timeouts, circuit breakers) cannot accidentally lose state.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxOptions.cs:13`, `:22`, `:25`; `docs/requirements/Component-NotificationOutbox.md:152-160` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxOptions.cs:13`, `:22`, `:25`; `docs/requirements/Component-NotificationOutbox.md:152-160` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -363,7 +363,7 @@ remains tracked separately.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs:29-31`, `:251-259`; tests in `tests/ScadaLink.NotificationOutbox.Tests/NotificationOutboxActorDispatchTests.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs:29-31`, `:251-259`; tests in `tests/ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests/NotificationOutboxActorDispatchTests.cs` |
|
||||
|
||||
**Resolution (2026-05-28):** The `FallbackMaxRetries` / `FallbackRetryDelay` constants are documented for forward-compat with a deferred secondary-adapter path — when no SMTP configuration row exists, `EmailNotificationDeliveryAdapter` returns `Permanent("No SMTP configuration available")` before the retry-policy values are ever consulted, so the path is effectively unreachable from any current production caller. The reachable use of the constants — clamping a non-positive `SmtpConfiguration.MaxRetries` / `RetryDelay` (per NO-002) — is already covered by `TransientFailure_WithZeroMaxRetries_RetriesUsingFallback_DoesNotParkImmediately`, `TransientFailure_WithNegativeMaxRetries_RetriesUsingFallback_DoesNotParkImmediately`, and `TransientFailure_WithNonPositiveRetryDelay_UsesFallbackDelay_NotZero` in `NotificationOutboxActorDispatchTests.cs`. Mark untestable today and re-visit when a non-Email adapter (Teams etc.) makes the empty-SMTP-config branch genuinely deliverable.
|
||||
|
||||
@@ -401,7 +401,7 @@ the choice in the actor XML so a maintainer does not "fix" the unreachable code.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxOptions.cs:15-16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxOptions.cs:15-16` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -440,7 +440,7 @@ requeue, or escalation. Matches `Component-NotificationOutbox.md §Monitoring`.
|
||||
| Severity | Medium |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationOutbox/NotificationOutboxActor.cs:469-477` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs:469-477` |
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.NotificationService` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.NotificationService` |
|
||||
| Design doc | `docs/requirements/Component-NotificationService.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -124,7 +124,7 @@ prefix could be aggressively scrubbed out of unrelated log text (NS-025).
|
||||
| Severity | Critical |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:96`, `src/ScadaLink.NotificationService/ServiceCollectionExtensions.cs:8` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:96`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/ServiceCollectionExtensions.cs:8` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -153,7 +153,7 @@ path. Fixed by the commit whose message references `NotificationService-001`.
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:157-167` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:157-167` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -181,7 +181,7 @@ plus `SocketException`/`TimeoutException` are treated as transient. Regression t
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:144-147`, `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:163-166` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:144-147`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:163-166` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -211,7 +211,7 @@ Regression tests `Send_Smtp5xxCommandException_ClassifiedPermanent`,
|
||||
| Severity | High |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:118-119` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:118-119` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -243,7 +243,7 @@ the resulting client is disposed.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/MailKitSmtpClientWrapper.cs:18`, `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:123` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs:18`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:123` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -273,7 +273,7 @@ parks a buffered message) instead of silently negotiating TLS. Regression tests:
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/OAuth2TokenService.cs:14-15`, `src/ScadaLink.NotificationService/OAuth2TokenService.cs:30-35` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/OAuth2TokenService.cs:14-15`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/OAuth2TokenService.cs:30-35` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -301,7 +301,7 @@ and `GetTokenAsync_SameCredentials_CachedPerCredential`.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationOptions.cs:11-14`, `src/ScadaLink.NotificationService/MailKitSmtpClientWrapper.cs:16-20`, `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:111-140` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationOptions.cs:11-14`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs:16-20`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:111-140` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -332,7 +332,7 @@ remain as operational fallback defaults. Regression tests:
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:136-137`, `src/ScadaLink.NotificationService/MailKitSmtpClientWrapper.cs:50-53` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:136-137`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs:50-53` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -362,7 +362,7 @@ the Central UI is a separate component and out of this module's scope.
|
||||
| Severity | Medium — re-triaged: split into an in-scope log-leak fix (resolved) and a Commons-scoped at-rest-encryption / structured-credential follow-up (NotificationService-013, Deferred). |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:127-134`, `src/ScadaLink.NotificationService/OAuth2TokenService.cs:30-65`, `src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs:9` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:127-134`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/OAuth2TokenService.cs:30-65`, `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmtpConfiguration.cs:9` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -389,7 +389,7 @@ conflates two concerns with different ownership.
|
||||
2. **At-rest encryption + structured-credential modelling (out of scope — Deferred).**
|
||||
Encrypting `SmtpConfiguration.Credentials` at rest and replacing the brittle
|
||||
colon-packed `string` with structured fields requires editing
|
||||
`src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs` and the
|
||||
`src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmtpConfiguration.cs` and the
|
||||
ConfigurationDatabase EF layer — both outside this module. Tracked separately as
|
||||
**NotificationService-013** (Deferred) so it is not lost.
|
||||
|
||||
@@ -400,7 +400,7 @@ conflates two concerns with different ownership.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Deferred |
|
||||
| Location | `src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs:9`, ConfigurationDatabase EF mapping |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmtpConfiguration.cs:9`, ConfigurationDatabase EF mapping |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -419,7 +419,7 @@ fields directly.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Deferred — requires changes to `src/ScadaLink.Commons` and the ConfigurationDatabase
|
||||
Deferred — requires changes to `src/ZB.MOM.WW.ScadaBridge.Commons` and the ConfigurationDatabase
|
||||
component, which are outside the NotificationService module. To be addressed in a
|
||||
Commons/ConfigurationDatabase-scoped change. The associated log-leak risk is resolved
|
||||
under NotificationService-009.
|
||||
@@ -431,7 +431,7 @@ under NotificationService-009.
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:121-154` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:121-154` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -461,7 +461,7 @@ mask the original delivery exception. Regression tests
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:173-177`, `src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs:5-15` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:173-177`, `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Notifications/SmtpConfiguration.cs:5-15` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -477,11 +477,11 @@ Resolved 2026-05-16 (commit pending). Both issues confirmed against source.
|
||||
|
||||
1. **`SmtpPermanentException` placement (in scope — fixed).** The public exception type
|
||||
was extracted from the bottom of `NotificationDeliveryService.cs` into its own file,
|
||||
`src/ScadaLink.NotificationService/SmtpPermanentException.cs`, restoring the
|
||||
`src/ZB.MOM.WW.ScadaBridge.NotificationService/SmtpPermanentException.cs`, restoring the
|
||||
one-type-per-file layout. No behaviour change, so no dedicated regression test — the
|
||||
move is verified by the module test suite still compiling and passing (56 tests green).
|
||||
2. **`SmtpConfiguration` non-nullable strings (out of scope — re-triaged).**
|
||||
`SmtpConfiguration` lives in `src/ScadaLink.Commons`, which is outside the
|
||||
`SmtpConfiguration` lives in `src/ZB.MOM.WW.ScadaBridge.Commons`, which is outside the
|
||||
NotificationService module's edit scope; it cannot be changed here. This is the same
|
||||
Commons entity already flagged for follow-up under **NotificationService-013**
|
||||
(Deferred). The `required`-members / documented-constructor change should be folded into
|
||||
@@ -497,7 +497,7 @@ Resolved 2026-05-16 (commit pending). Both issues confirmed against source.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.NotificationService.Tests/NotificationDeliveryServiceTests.cs`, `tests/ScadaLink.NotificationService.Tests/OAuth2TokenServiceTests.cs` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/NotificationDeliveryServiceTests.cs`, `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/OAuth2TokenServiceTests.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -543,7 +543,7 @@ Module test suite is green at 56 tests.
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:214-228`, `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:308-312`, `src/ScadaLink.NotificationService/OAuth2TokenService.cs:56-84` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:214-228`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:308-312`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/OAuth2TokenService.cs:56-84` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -564,7 +564,7 @@ Resolved 2026-05-17. Root cause confirmed against source — `DeliverBufferedAsy
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:96-148`, `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:308-312` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:96-148`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:308-312` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -585,7 +585,7 @@ Resolved 2026-05-17. Root cause confirmed — `SendAsync` had only three catch c
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/MailKitSmtpClientWrapper.cs:46-67` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs:46-67` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -606,11 +606,11 @@ Resolved 2026-05-17. Root cause confirmed — `AuthenticateAsync` returned silen
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationOptions.cs:1-15`, `src/ScadaLink.NotificationService/ServiceCollectionExtensions.cs:10-11`, `src/ScadaLink.Host/SiteServiceRegistration.cs:70` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationOptions.cs:1-15`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/ServiceCollectionExtensions.cs:10-11`, `src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs:70` |
|
||||
|
||||
**Description**
|
||||
|
||||
`NotificationOptions` (with `ConnectionTimeoutSeconds` and `MaxConcurrentConnections`) is bound from the `ScadaLink:Notification` configuration section in two places — `ServiceCollectionExtensions.AddNotificationService` (`AddOptions<NotificationOptions>().BindConfiguration(...)`) and again in `Host/SiteServiceRegistration.cs:70` (`services.Configure<NotificationOptions>(...)`). However, a repo-wide search shows no code ever injects `IOptions<NotificationOptions>` or otherwise reads either property. When NS-007 enforced the connection timeout and concurrency limit, it sourced both values from the per-site `SmtpConfiguration` entity, not from `NotificationOptions` — so this options class is now entirely dead configuration. Its XML doc still claims it "provides fallback defaults and operational limits", which is misleading: nothing falls back to it. The double binding is also redundant. Dead, falsely-documented configuration invites an operator to set `ScadaLink:Notification:ConnectionTimeoutSeconds` and expect it to take effect, when it has no effect at all.
|
||||
`NotificationOptions` (with `ConnectionTimeoutSeconds` and `MaxConcurrentConnections`) is bound from the `ScadaBridge:Notification` configuration section in two places — `ServiceCollectionExtensions.AddNotificationService` (`AddOptions<NotificationOptions>().BindConfiguration(...)`) and again in `Host/SiteServiceRegistration.cs:70` (`services.Configure<NotificationOptions>(...)`). However, a repo-wide search shows no code ever injects `IOptions<NotificationOptions>` or otherwise reads either property. When NS-007 enforced the connection timeout and concurrency limit, it sourced both values from the per-site `SmtpConfiguration` entity, not from `NotificationOptions` — so this options class is now entirely dead configuration. Its XML doc still claims it "provides fallback defaults and operational limits", which is misleading: nothing falls back to it. The double binding is also redundant. Dead, falsely-documented configuration invites an operator to set `ScadaBridge:Notification:ConnectionTimeoutSeconds` and expect it to take effect, when it has no effect at all.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
@@ -627,7 +627,7 @@ Resolved 2026-05-17. Root cause confirmed — `NotificationOptions` was bound bu
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:237-255` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:237-255` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -648,9 +648,9 @@ Resolved 2026-05-17. All three issues confirmed against source. The hand-rolled
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:18-442`, `src/ScadaLink.NotificationService/ServiceCollectionExtensions.cs:20-21`, `src/ScadaLink.Commons/Interfaces/Services/INotificationDeliveryService.cs:1-33`, `src/ScadaLink.Host/Program.cs:77` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:18-442`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/ServiceCollectionExtensions.cs:20-21`, `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/INotificationDeliveryService.cs:1-33`, `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:77` |
|
||||
|
||||
**Resolution** — Executed option 1. Deleted `src/ScadaLink.NotificationService/NotificationDeliveryService.cs`, `src/ScadaLink.Commons/Interfaces/Services/INotificationDeliveryService.cs` (also retires `NotificationResult` + `BufferedNotification`), and the orphaned `tests/ScadaLink.NotificationService.Tests/NotificationDeliveryServiceTests.cs` suite; reduced `AddNotificationService` to the shared SMTP primitives (`OAuth2TokenService`, `Func<ISmtpClientWrapper>`, `NotificationOptions`), updated `CompositionRootTests` (assert the primitives instead of the dead types), and removed the `Notification_Send_MockSmtp_Delivers` assertion in `IntegrationSurfaceTests` (central delivery is covered by `EmailNotificationDeliveryAdapterTests`). Grep-verified `grep -rn "INotificationDeliveryService\|NotificationDeliveryService\|NotificationResult\|BufferedNotification\|DeliverBufferedAsync" --include="*.cs" src/ tests/` before delete: zero production callers (only XML-doc cross-references in NS, MailKit wrapper, NotificationOptions and `EmailNotificationDeliveryAdapter`, plus the dead test files); cross-reference comments updated to remove the stale class references. `dotnet build ScadaLink.slnx` succeeds (0 warnings, 0 errors); affected test projects all pass (`NotificationService.Tests` 52/52, `NotificationOutbox.Tests` 86/86 on rerun — one flaky timing-sensitive Akka.TestKit test unrelated to NS-019, `Host.Tests` 205/205); `IntegrationTests` 64/66 with two pre-existing failures in `NotificationOutboxFlowTests` (SQLite "near IF: syntax error", reproducible on pristine `main`, unrelated to NS-019).
|
||||
**Resolution** — Executed option 1. Deleted `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs`, `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/INotificationDeliveryService.cs` (also retires `NotificationResult` + `BufferedNotification`), and the orphaned `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/NotificationDeliveryServiceTests.cs` suite; reduced `AddNotificationService` to the shared SMTP primitives (`OAuth2TokenService`, `Func<ISmtpClientWrapper>`, `NotificationOptions`), updated `CompositionRootTests` (assert the primitives instead of the dead types), and removed the `Notification_Send_MockSmtp_Delivers` assertion in `IntegrationSurfaceTests` (central delivery is covered by `EmailNotificationDeliveryAdapterTests`). Grep-verified `grep -rn "INotificationDeliveryService\|NotificationDeliveryService\|NotificationResult\|BufferedNotification\|DeliverBufferedAsync" --include="*.cs" src/ tests/` before delete: zero production callers (only XML-doc cross-references in NS, MailKit wrapper, NotificationOptions and `EmailNotificationDeliveryAdapter`, plus the dead test files); cross-reference comments updated to remove the stale class references. `dotnet build ZB.MOM.WW.ScadaBridge.slnx` succeeds (0 warnings, 0 errors); affected test projects all pass (`NotificationService.Tests` 52/52, `NotificationOutbox.Tests` 86/86 on rerun — one flaky timing-sensitive Akka.TestKit test unrelated to NS-019, `Host.Tests` 205/205); `IntegrationTests` 64/66 with two pre-existing failures in `NotificationOutboxFlowTests` (SQLite "near IF: syntax error", reproducible on pristine `main`, unrelated to NS-019).
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -660,8 +660,8 @@ The current source does not match. `NotificationDeliveryService` is a site-shape
|
||||
|
||||
Who actually calls it?
|
||||
|
||||
- **Sites** do **not**. `SiteServiceRegistration.cs:33-38` documents the deliberate omission: "AddNotificationService() is intentionally NOT registered on the site path." Sites register `NotificationForwarder` (in `ScadaLink.StoreAndForward`) as the S&F handler for `StoreAndForwardCategory.Notification` (`AkkaHostedService.cs:654-660`), which Asks the central comms actor and never touches SMTP. `ScriptRuntimeContext.NotifyHelper` (in `SiteRuntime`) enqueues directly to S&F as a serialized `NotificationSubmit`, **not** via `INotificationDeliveryService.SendAsync`.
|
||||
- **Central** registers it (`Program.cs:77` calls `AddNotificationService`) but no central component resolves it. The central notification dispatcher is `NotificationOutboxActor` → `INotificationDeliveryAdapter` → `EmailNotificationDeliveryAdapter`. The adapter is a full re-implementation of the connect/auth/send/disconnect sequence (see `EmailNotificationDeliveryAdapter.cs:163-222`) — it deliberately does not call `NotificationDeliveryService.DeliverAsync` (XML-doc on the adapter says "Reuses the `ScadaLink.NotificationService` SMTP machinery — `ISmtpClientWrapper`, `SmtpTlsModeParser`, `OAuth2TokenService` and the typed `SmtpPermanentException`", i.e. only the leaf primitives).
|
||||
- **Sites** do **not**. `SiteServiceRegistration.cs:33-38` documents the deliberate omission: "AddNotificationService() is intentionally NOT registered on the site path." Sites register `NotificationForwarder` (in `ZB.MOM.WW.ScadaBridge.StoreAndForward`) as the S&F handler for `StoreAndForwardCategory.Notification` (`AkkaHostedService.cs:654-660`), which Asks the central comms actor and never touches SMTP. `ScriptRuntimeContext.NotifyHelper` (in `SiteRuntime`) enqueues directly to S&F as a serialized `NotificationSubmit`, **not** via `INotificationDeliveryService.SendAsync`.
|
||||
- **Central** registers it (`Program.cs:77` calls `AddNotificationService`) but no central component resolves it. The central notification dispatcher is `NotificationOutboxActor` → `INotificationDeliveryAdapter` → `EmailNotificationDeliveryAdapter`. The adapter is a full re-implementation of the connect/auth/send/disconnect sequence (see `EmailNotificationDeliveryAdapter.cs:163-222`) — it deliberately does not call `NotificationDeliveryService.DeliverAsync` (XML-doc on the adapter says "Reuses the `ZB.MOM.WW.ScadaBridge.NotificationService` SMTP machinery — `ISmtpClientWrapper`, `SmtpTlsModeParser`, `OAuth2TokenService` and the typed `SmtpPermanentException`", i.e. only the leaf primitives).
|
||||
|
||||
The `NotificationDeliveryService` class, its `DeliverBufferedAsync`, the `Func<ISmtpClientWrapper>` registration consumed only by it, and the `INotificationDeliveryService` interface (still in Commons) and `NotificationResult` record are therefore dead code that contradicts the design. Worse, every prior finding NS-001..NS-018 was reviewed and resolved against this dead path. The 56-test green test suite (NS-012 resolution note) exercises behaviour no production caller invokes — it gives a false sense of coverage. The misleading XML doc on `NotificationDeliveryService` ("WP-11/12: Notification delivery via SMTP") tells a maintainer this is *the* delivery path; the registration on central does the same.
|
||||
|
||||
@@ -671,7 +671,7 @@ Risk: an operator following the design doc will look here for "the central email
|
||||
|
||||
Decide and execute one of:
|
||||
|
||||
1. **Delete `NotificationDeliveryService`, `DeliverBufferedAsync`, the `BufferedNotification` payload type, the `Func<ISmtpClientWrapper>` scoped registration (move it to NotificationOutbox if still needed there — it already has its own), and `INotificationDeliveryService`/`NotificationResult` in Commons.** Reduce `AddNotificationService` to registering the shared primitives — `OAuth2TokenService`, `ISmtpClientWrapper` factory, `NotificationOptions`. Delete the NS-001..NS-018 tests that target the orphaned path; rebase the ones that exercise primitives (`SmtpErrorClassifier`, `SmtpTlsModeParser`, `CredentialRedactor`, `EmailAddressValidator`, `MailKitSmtpClientWrapper`, `OAuth2TokenService`) which remain genuinely shared. Update `CompositionRootTests` (`tests/ScadaLink.Host.Tests/CompositionRootTests.cs:208-209`) and `IntegrationSurfaceTests` (`tests/ScadaLink.IntegrationTests/IntegrationSurfaceTests.cs:122-135`) to drop the stale assertions.
|
||||
1. **Delete `NotificationDeliveryService`, `DeliverBufferedAsync`, the `BufferedNotification` payload type, the `Func<ISmtpClientWrapper>` scoped registration (move it to NotificationOutbox if still needed there — it already has its own), and `INotificationDeliveryService`/`NotificationResult` in Commons.** Reduce `AddNotificationService` to registering the shared primitives — `OAuth2TokenService`, `ISmtpClientWrapper` factory, `NotificationOptions`. Delete the NS-001..NS-018 tests that target the orphaned path; rebase the ones that exercise primitives (`SmtpErrorClassifier`, `SmtpTlsModeParser`, `CredentialRedactor`, `EmailAddressValidator`, `MailKitSmtpClientWrapper`, `OAuth2TokenService`) which remain genuinely shared. Update `CompositionRootTests` (`tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs:208-209`) and `IntegrationSurfaceTests` (`tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/IntegrationSurfaceTests.cs:122-135`) to drop the stale assertions.
|
||||
|
||||
2. **Keep the class as the central-only Email delivery primitive** and rewrite `EmailNotificationDeliveryAdapter` to delegate to it. This is the smaller diff but the larger semantic burden — `NotificationDeliveryService.SendAsync` returns `NotificationResult` (Success / WasBuffered) which cannot encode the three-way `DeliveryOutcome` (Success / Transient / Permanent) the outbox needs, so the contract still has to change.
|
||||
|
||||
@@ -684,7 +684,7 @@ Recommended path is option 1: the parallel implementation in `EmailNotificationD
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:654-660`, NS-001 resolution note (this file) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:654-660`, NS-001 resolution note (this file) |
|
||||
|
||||
**Resolution (2026-05-28):** Added a `_notificationDeliveryHandlerRegistered` sentinel field on `AkkaHostedService` and gated the canonical `NotificationForwarder` registration with an `InvalidOperationException` guard — a future code path that re-introduces the dead NS-001 site-SMTP handler now fails fast at startup with an explicit NS-020 diagnostic, rather than silently overwriting `RegisterDeliveryHandler`'s last-write-wins map and inverting the central-only design. The sentinel's XML doc cross-references NS-001/NS-019/NS-020 so a maintainer searching for the `Notification` S&F handler finds the one canonical registration and its history.
|
||||
|
||||
@@ -707,7 +707,7 @@ Mark the NS-001 resolution note in this file as **superseded by NS-019** with a
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/MailKitSmtpClientWrapper.cs:76-79` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs:76-79` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -736,7 +736,7 @@ Pass the sender mailbox into the wrapper's `AuthenticateAsync` path. The cleanes
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/MailKitSmtpClientWrapper.cs:14`, `src/ScadaLink.NotificationService/ServiceCollectionExtensions.cs:19` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs:14`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/ServiceCollectionExtensions.cs:19` |
|
||||
|
||||
**Resolution (2026-05-28):** Took option (a) — updated the class-level XML doc on `MailKitSmtpClientWrapper` with an explicit lifetime section (NS-022) describing single-connection ownership and the per-delivery factory contract (NOT a pool; `MailKit.Net.Smtp.SmtpClient` holds one TCP/TLS connection and is not thread-safe; the DI `Func<ISmtpClientWrapper>` is a factory, callers run connect/auth/send/disconnect/dispose per send). A field-level comment was added to the `_client` declaration cross-referencing the class docs so a maintainer hunting from the field-initializer immediately sees the constraint. The wrapper code itself is unchanged — option (b) (transient SmtpClient per Send) was deliberately not taken because it would re-handshake TLS per email which is materially more expensive than the current per-delivery factory model the callers already implement. The concurrent-connection limit regression on the central-side delivery path (per-site semaphore on `EmailNotificationDeliveryAdapter`) is out of NotificationService scope and tracked separately.
|
||||
|
||||
@@ -761,7 +761,7 @@ Document the per-send lifecycle on `MailKitSmtpClientWrapper` (XML on the class:
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:12-17`, `src/ScadaLink.Commons/Interfaces/Services/INotificationDeliveryService.cs:3-12`, `src/ScadaLink.NotificationService/ServiceCollectionExtensions.cs:8-9` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:12-17`, `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/INotificationDeliveryService.cs:3-12`, `src/ZB.MOM.WW.ScadaBridge.NotificationService/ServiceCollectionExtensions.cs:8-9` |
|
||||
|
||||
**Resolution (2026-05-28):** Closed by NS-019 — both `NotificationDeliveryService.cs` and `INotificationDeliveryService.cs` were removed in commit `ac96b83`, and `ServiceCollectionExtensions.AddNotificationService`'s XML doc was rewritten in the same commit to describe the central-only design (shared SMTP primitives consumed by `EmailNotificationDeliveryAdapter`, with an explicit NS-019 cross-reference and a note that sites no longer deliver notifications). No stale XML docs remain in this module.
|
||||
|
||||
@@ -787,9 +787,9 @@ Tied to NS-019: if the orphan classes are deleted, this finding closes itself. I
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.NotificationService.Tests/NotificationDeliveryServiceTests.cs`, `tests/ScadaLink.IntegrationTests/IntegrationSurfaceTests.cs:118-136`, `tests/ScadaLink.Host.Tests/CompositionRootTests.cs:207-209` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/NotificationDeliveryServiceTests.cs`, `tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/IntegrationSurfaceTests.cs:118-136`, `tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs:207-209` |
|
||||
|
||||
**Resolution (2026-05-28):** Closed by NS-019 — the orphaned `NotificationDeliveryService` class, its `INotificationDeliveryService` Commons interface, and the associated `NotificationDeliveryServiceTests.cs` test file (~40 tests asserting `SendAsync`/`DeliverBufferedAsync` behaviour against a code path no production caller resolves) were all deleted in the NS-019 fix commit. Verification: a directory listing of `tests/ScadaLink.NotificationService.Tests/` shows only `CredentialRedactorTests.cs`, `MailKitSmtpClientWrapperTests.cs`, `NotificationOptionsTests.cs`, `OAuth2TokenServiceTests.cs`, `SmtpErrorClassifierTests.cs`, and `SmtpTlsModeParserTests.cs` — every retained file exercises a primitive that the central NotificationOutbox `EmailNotificationDeliveryAdapter` still depends on, so the false-coverage signal this finding called out no longer exists. The "no test affirms the central-only invariant" gap was the consequence of the orphaned tests existing; with them gone, the module test suite genuinely scopes to the shared SMTP primitives. The architecture-test recommendation (banning new consumers of `INotificationDeliveryService`) is moot once the interface itself is gone.
|
||||
**Resolution (2026-05-28):** Closed by NS-019 — the orphaned `NotificationDeliveryService` class, its `INotificationDeliveryService` Commons interface, and the associated `NotificationDeliveryServiceTests.cs` test file (~40 tests asserting `SendAsync`/`DeliverBufferedAsync` behaviour against a code path no production caller resolves) were all deleted in the NS-019 fix commit. Verification: a directory listing of `tests/ZB.MOM.WW.ScadaBridge.NotificationService.Tests/` shows only `CredentialRedactorTests.cs`, `MailKitSmtpClientWrapperTests.cs`, `NotificationOptionsTests.cs`, `OAuth2TokenServiceTests.cs`, `SmtpErrorClassifierTests.cs`, and `SmtpTlsModeParserTests.cs` — every retained file exercises a primitive that the central NotificationOutbox `EmailNotificationDeliveryAdapter` still depends on, so the false-coverage signal this finding called out no longer exists. The "no test affirms the central-only invariant" gap was the consequence of the orphaned tests existing; with them gone, the module test suite genuinely scopes to the shared SMTP primitives. The architecture-test recommendation (banning new consumers of `INotificationDeliveryService`) is moot once the interface itself is gone.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -799,7 +799,7 @@ In particular there is **no test in this module** that affirms the central-only
|
||||
|
||||
- No test that `AddNotificationService()` registered on a *site* role would be inert / no-op'd, or that `SiteServiceRegistration.Configure` does **not** call `AddNotificationService` (an obvious regression vector — re-adding it would silently restore the orphaned site-delivery path).
|
||||
- No test that confirms `INotificationDeliveryService` has no production consumer (i.e. an architecture test that fails if anyone re-introduces a constructor parameter or `GetRequiredService<INotificationDeliveryService>()` call).
|
||||
- The cross-module `CompositionRootTests` (`tests/ScadaLink.Host.Tests/CompositionRootTests.cs:208-209`) still asserts `NotificationDeliveryService` and `INotificationDeliveryService` are registered, locking in the orphan rather than catching it.
|
||||
- The cross-module `CompositionRootTests` (`tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs:208-209`) still asserts `NotificationDeliveryService` and `INotificationDeliveryService` are registered, locking in the orphan rather than catching it.
|
||||
- `IntegrationSurfaceTests.cs:122-125` constructs `NotificationDeliveryService` directly to validate "the integration surface" — testing a surface that no script actually crosses.
|
||||
|
||||
**Recommendation**
|
||||
@@ -807,7 +807,7 @@ In particular there is **no test in this module** that affirms the central-only
|
||||
After NS-019 is decided:
|
||||
|
||||
1. If the orphan is deleted, remove the orphaned-path tests (NS-001/004/005/007/008/009/010/014/015/016/017/018-style tests targeting `SendAsync`/`DeliverBufferedAsync`). Retain `SmtpErrorClassifierTests`, `SmtpTlsModeParserTests`, `CredentialRedactorTests`, `OAuth2TokenServiceTests`, and `MailKitSmtpClientWrapperTests` (primitives genuinely shared). Update `CompositionRootTests` to drop the stale rows and `IntegrationSurfaceTests` to call the live path via `INotificationDeliveryAdapter`/`EmailNotificationDeliveryAdapter`.
|
||||
2. Add a one-shot architecture test in `tests/ScadaLink.Architecture.Tests` (if it exists, else this module) that scans for direct references to `INotificationDeliveryService` outside this project and the obsolete-interface declaration in Commons, failing if any new consumer reappears.
|
||||
2. Add a one-shot architecture test in `tests/ZB.MOM.WW.ScadaBridge.Architecture.Tests` (if it exists, else this module) that scans for direct references to `INotificationDeliveryService` outside this project and the obsolete-interface declaration in Commons, failing if any new consumer reappears.
|
||||
|
||||
### NotificationService-025 — `CredentialRedactor` over-masks: any 4-character credential component is masked anywhere it appears, including unrelated log text
|
||||
|
||||
@@ -816,7 +816,7 @@ After NS-019 is decided:
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.NotificationService/CredentialRedactor.cs:34-48` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.NotificationService/CredentialRedactor.cs:34-48` |
|
||||
|
||||
**Resolution (2026-05-28):** Tightened the policy per the recommendation —
|
||||
only the LAST colon-separated component (password in Basic / clientSecret
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Code Reviews
|
||||
|
||||
Comprehensive, per-module code reviews of the ScadaLink codebase. Each module (one
|
||||
Comprehensive, per-module code reviews of the ScadaBridge codebase. Each module (one
|
||||
buildable project under `src/`) has its own folder containing a `findings.md`. This
|
||||
README is the aggregated index — the single place to see all outstanding work.
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Code Review Process
|
||||
|
||||
This document describes how to perform a comprehensive, per-module code review of
|
||||
the ScadaLink codebase and how to track findings to resolution.
|
||||
the ScadaBridge codebase and how to track findings to resolution.
|
||||
|
||||
A **module** is one buildable project under `src/` (e.g. `src/ScadaLink.TemplateEngine`).
|
||||
A **module** is one buildable project under `src/` (e.g. `src/ZB.MOM.WW.ScadaBridge.TemplateEngine`).
|
||||
Each module has its own folder under `code-reviews/` containing a single `findings.md`.
|
||||
|
||||
## 1. Before you start
|
||||
|
||||
1. Pick the module to review. Its folder is `code-reviews/<Module>/` where `<Module>`
|
||||
is the project name with the `ScadaLink.` prefix stripped.
|
||||
is the project name with the `ZB.MOM.WW.ScadaBridge.` prefix stripped.
|
||||
2. Identify the design context for the module:
|
||||
- Its component design doc: `docs/requirements/Component-<Name>.md`.
|
||||
- The relevant **Key Design Decisions** in `CLAUDE.md`.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.Security` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.Security` |
|
||||
| Design doc | `docs/requirements/Component-Security.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -117,7 +117,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:37-47` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs:37-47` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -153,7 +153,7 @@ and `AuthenticateAsync_NoTlsTransport_RejectedWithoutAllowInsecure`.
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/ServiceCollectionExtensions.cs:16-23` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs:16-23` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -186,7 +186,7 @@ tuning left as a separate, lower-priority improvement.)
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/JwtTokenService.cs:33`, `src/ScadaLink.Security/SecurityOptions.cs:42` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/JwtTokenService.cs:33`, `src/ZB.MOM.WW.ScadaBridge.Security/SecurityOptions.cs:42` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -222,7 +222,7 @@ corrected to state the requirement. Regression tests
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:66`, `:138`, `:157-159` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs:66`, `:138`, `:157-159` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -258,7 +258,7 @@ Regression tests `BuildFallbackUserDn_UsesConfiguredUserIdAttribute`,
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:157-159` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs:157-159` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -297,7 +297,7 @@ Regression tests `BuildFallbackUserDn_EscapesDnMetacharacters`,
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/JwtTokenService.cs:67-75`, `:56-59` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/JwtTokenService.cs:67-75`, `:56-59` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -311,7 +311,7 @@ silently exploitable.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Set a fixed `Issuer` and `Audience` (e.g. `"scadalink-central"`) when generating tokens
|
||||
Set a fixed `Issuer` and `Audience` (e.g. `"scadabridge-central"`) when generating tokens
|
||||
and enable `ValidateIssuer`/`ValidateAudience` with the matching expected values during
|
||||
validation.
|
||||
|
||||
@@ -320,7 +320,7 @@ validation.
|
||||
Resolved 2026-05-16 (commit `pending`). Confirmed: `GenerateToken` set neither `iss`
|
||||
nor `aud` and `ValidateToken` had `ValidateIssuer = false`/`ValidateAudience = false`.
|
||||
`GenerateToken` now binds `JwtTokenService.TokenIssuer`/`TokenAudience`
|
||||
(both `"scadalink-central"`) into every token, and `ValidateToken` enables
|
||||
(both `"scadabridge-central"`) into every token, and `ValidateToken` enables
|
||||
`ValidateIssuer`/`ValidateAudience` against those fixed values — a token signed with
|
||||
the shared key but a foreign issuer is now rejected. Regression tests
|
||||
`GenerateToken_SetsIssuerAndAudience`, `ValidateToken_RejectsTokenWithWrongIssuer`,
|
||||
@@ -333,7 +333,7 @@ the shared key but a foreign issuer is now rejected. Regression tests
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/JwtTokenService.cs:40`, `:111-123` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/JwtTokenService.cs:40`, `:111-123` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -381,7 +381,7 @@ Security-side defect — the reset-on-refresh bug — is fully fixed here. Regre
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Deferred |
|
||||
| Location | `src/ScadaLink.Security/RoleMapper.cs:25-48` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/RoleMapper.cs:25-48` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -403,7 +403,7 @@ issues one `GetScopeRulesForMappingAsync` round-trip per matched Deployment mapp
|
||||
genuine N+1 on the login / 15-minute-refresh path. However, the only correct fix
|
||||
(a batch `GetScopeRulesForMappingsAsync(IEnumerable<int>)` repository method, or an
|
||||
eager-load navigation property) requires changes to `ISecurityRepository`
|
||||
(`src/ScadaLink.Commons`) and `SecurityRepository` (`src/ScadaLink.ConfigurationDatabase`).
|
||||
(`src/ZB.MOM.WW.ScadaBridge.Commons`) and `SecurityRepository` (`src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase`).
|
||||
Both are outside the Security module's permitted edit scope for this review pass, and the
|
||||
existing `ISecurityRepository` surface offers no per-set scope-rule query, so the N+1
|
||||
cannot be removed from within `RoleMapper.cs` alone. Severity is Low (bounded by the
|
||||
@@ -418,7 +418,7 @@ rules in a single call.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:42`, `:46`, `:51`, `:56-57`, `:67-73`, `:135`, `:139-145` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs:42`, `:46`, `:51`, `:56-57`, `:67-73`, `:135`, `:139-145` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -490,7 +490,7 @@ of the document and the code.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.Security.Tests/UnitTest1.cs` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.Security.Tests/UnitTest1.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -529,7 +529,7 @@ LDAP-timeout coverage (Security-009) plus extra no-service-account / DN-path edg
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:78-118` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs:78-118` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -581,7 +581,7 @@ treating a genuine empty-groups result as a successful login. Regression tests
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:258-269` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs:258-269` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -621,7 +621,7 @@ testing. Regression tests `ExtractFirstRdnValue_EscapedComma_KeepsWholeGroupName
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/JwtTokenService.cs:156-169` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/JwtTokenService.cs:156-169` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -665,7 +665,7 @@ invariant. Regression tests `RefreshToken_IdleExpiredPrincipal_ReturnsNull`,
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:20-21`, `:80`, `:122`, `:169`, `:193` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs:20-21`, `:80`, `:122`, `:169`, `:193` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -707,7 +707,7 @@ use the single canonical identity. Regression tests
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/RoleMapper.cs:30-31`, `:41-55`, `:59` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/RoleMapper.cs:30-31`, `:41-55`, `:59` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -765,17 +765,17 @@ after.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/SiteScopeAuthorizationHandler.cs:8-58`; `src/ScadaLink.Security/AuthorizationPolicies.cs:113-143` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/SiteScopeAuthorizationHandler.cs:8-58`; `src/ZB.MOM.WW.ScadaBridge.Security/AuthorizationPolicies.cs:113-143` |
|
||||
|
||||
**Description**
|
||||
|
||||
The module declares `SiteScopeRequirement` (an `IAuthorizationRequirement` carrying a
|
||||
`TargetSiteId`) and the matching `SiteScopeAuthorizationHandler` that combines the
|
||||
Deployment role claim with the `SiteId` claims to enforce the design's site-scoping
|
||||
rule. The handler is registered in `AddScadaLinkAuthorization`
|
||||
rule. The handler is registered in `AddScadaBridgeAuthorization`
|
||||
(`services.AddSingleton<IAuthorizationHandler, SiteScopeAuthorizationHandler>()`). But
|
||||
no `AddPolicy` call ever wires the requirement to a named policy, and a grep across
|
||||
`src/ScadaLink.CentralUI` and `src/ScadaLink.ManagementService` confirms that **no
|
||||
`src/ZB.MOM.WW.ScadaBridge.CentralUI` and `src/ZB.MOM.WW.ScadaBridge.ManagementService` confirms that **no
|
||||
production code ever instantiates `new SiteScopeRequirement(...)` or calls
|
||||
`AuthorizeAsync(...)` with one** — the only callers are the unit tests in
|
||||
`SecurityTests.cs:1146,1166,1185,1203`. The design + CLAUDE.md state that "Deployment
|
||||
@@ -808,7 +808,7 @@ piecemeal.
|
||||
Resolved 2026-05-28 (commit pending) per recommendation option (a): deleted
|
||||
`SiteScopeRequirement` and `SiteScopeAuthorizationHandler` outright, along with the
|
||||
unwired `services.AddSingleton<IAuthorizationHandler, SiteScopeAuthorizationHandler>()`
|
||||
registration in `AuthorizationPolicies.AddScadaLinkAuthorization` and the four
|
||||
registration in `AuthorizationPolicies.AddScadaBridgeAuthorization` and the four
|
||||
isolation tests in `SecurityTests.cs`. `SiteScopeService` (CentralUI-002) is now
|
||||
documented as the sole site-scoping mechanism — the stale "mirrors
|
||||
SiteScopeAuthorizationHandler" comment in `SiteScopeService.ResolveAsync` was rewritten
|
||||
@@ -822,7 +822,7 @@ new report pages now consume `SiteScopeService`).
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/RoleMapper.cs:41`; `src/ScadaLink.Security/SiteScopeAuthorizationHandler.cs:36`; `src/ScadaLink.Security/AuthorizationPolicies.cs:118,121,124,95,107` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/RoleMapper.cs:41`; `src/ZB.MOM.WW.ScadaBridge.Security/SiteScopeAuthorizationHandler.cs:36`; `src/ZB.MOM.WW.ScadaBridge.Security/AuthorizationPolicies.cs:118,121,124,95,107` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -852,11 +852,11 @@ will then either succeed everywhere or fail to compile.
|
||||
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-28 (commit pending). Added `src/ScadaLink.Security/Roles.cs` holding
|
||||
Resolved 2026-05-28 (commit pending). Added `src/ZB.MOM.WW.ScadaBridge.Security/Roles.cs` holding
|
||||
`Admin`/`Design`/`Deployment`/`Audit`/`AuditReadOnly` as `public const string`
|
||||
fields. Replaced every magic-string occurrence in this module:
|
||||
`RoleMapper.MapGroupsToRolesAsync` now compares against `Roles.Deployment`;
|
||||
`AuthorizationPolicies.AddScadaLinkAuthorization` binds `RequireClaim(...)` to
|
||||
`AuthorizationPolicies.AddScadaBridgeAuthorization` binds `RequireClaim(...)` to
|
||||
`Roles.{Admin,Design,Deployment}`; `OperationalAuditRoles` /
|
||||
`AuditExportRoles` are now built from `Roles.Admin`, `Roles.Audit`, `Roles.AuditReadOnly`.
|
||||
`SiteScopeAuthorizationHandler.cs` was deleted under Security-017, so its
|
||||
@@ -870,7 +870,7 @@ single edit or fails to compile.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/LdapAuthService.cs:85-89`, `:147-151` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/LdapAuthService.cs:85-89`, `:147-151` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -903,7 +903,7 @@ distinct error message.
|
||||
**Resolution**
|
||||
|
||||
Resolved 2026-05-28 (commit pending). Added a new `ServiceAccountBindException`
|
||||
(`src/ScadaLink.Security/ServiceAccountBindException.cs`) — deliberately NOT an
|
||||
(`src/ZB.MOM.WW.ScadaBridge.Security/ServiceAccountBindException.cs`) — deliberately NOT an
|
||||
`LdapException` subtype so it short-circuits the generic LDAP catch — and a private
|
||||
`BindServiceAccountAsync` helper on `LdapAuthService` that wraps both service-account
|
||||
rebind sites (the post-user-bind group-lookup rebind AND the `ResolveUserDnAsync`
|
||||
@@ -926,7 +926,7 @@ is the closest meaningful unit-level coverage.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/SecurityOptions.cs:6-7`, `:36-37`; `src/ScadaLink.Security/ServiceCollectionExtensions.cs:13-30` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/SecurityOptions.cs:6-7`, `:36-37`; `src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs:13-30` |
|
||||
|
||||
**Resolution (2026-05-28):** added `SecurityOptionsValidator`
|
||||
(`IValidateOptions<SecurityOptions>`) that rejects empty/whitespace
|
||||
@@ -972,7 +972,7 @@ every required `SecurityOptions` field is enforced at startup, not at first use.
|
||||
| Severity | Low |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Security/SecurityOptions.cs:100-108`; `src/ScadaLink.Security/ServiceCollectionExtensions.cs:54-59` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Security/SecurityOptions.cs:100-108`; `src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs:54-59` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `ILoggerFactory` to the cookie-options
|
||||
`Configure` callback in `AddSecurity` so an explicit `RequireHttpsCookie=false`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.SiteCallAudit` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.SiteCallAudit` |
|
||||
| Design doc | `docs/requirements/Component-SiteCallAudit.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -51,7 +51,7 @@ tests using a shared `MsSqlMigrationFixture`.
|
||||
| Severity | Medium |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs:32-46`, `src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs:147-151` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs:32-46`, `src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs:147-151` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -107,7 +107,7 @@ children (Site Runtime hierarchy) — not to leaf cluster singletons.
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:455-462` (singleton wiring), `src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs:153-193` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:455-462` (singleton wiring), `src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs:153-193` |
|
||||
|
||||
**Resolution (2026-05-28):** Added a `CoordinatedShutdown` task in the `cluster-leave` phase (named `drain-site-call-audit-singleton`) that issues an explicit `GracefulStop(10s)` to the `SiteCallAudit` cluster singleton manager before the cluster-leave proceeds. Akka.NET's singleton handover already waits for the active actor's `ReceiveAsync` task to complete before signalling `HandOverDone`, so an in-flight EF `UpsertAsync` (and its SQL round-trip) drains on the old node before the new singleton starts on the other central node — closing the seam where the new singleton could race a still-running upsert on the old node. The 10-second timeout is bounded so a misbehaving upsert cannot stall coordinated shutdown indefinitely; on timeout the existing `PoisonPill` termination path takes over and the repository's monotonic-upsert + 2601/2627 duplicate-key swallow remain as the storage-state safety net. Pattern is suitable for the `NotificationOutbox` singleton too; deferred to keep this change scoped.
|
||||
|
||||
@@ -153,14 +153,14 @@ Notification Outbox sibling has the same pattern.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs:153-193` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs:153-193` |
|
||||
|
||||
**Description**
|
||||
|
||||
The combined-telemetry hot path (`AuditLogIngestActor.OnCachedTelemetryAsync`)
|
||||
stamps `IngestedAtUtc = DateTime.UtcNow` on both the `AuditLog` row and the
|
||||
`SiteCall` row at central-side persist time
|
||||
(`src/ScadaLink.AuditLog/Central/AuditLogIngestActor.cs:238-239`). The design
|
||||
(`src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs:238-239`). The design
|
||||
doc treats `IngestedAtUtc` as "central ingested (or last refreshed) this row"
|
||||
— a central-side timestamp.
|
||||
|
||||
@@ -197,7 +197,7 @@ because callers cannot in general know the actor is colocated on central.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs:23-30` (actor XML), `src/ScadaLink.SiteCallAudit/ServiceCollectionExtensions.cs:8-13`, `docs/requirements/Component-SiteCallAudit.md:24-32` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs:23-30` (actor XML), `src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/ServiceCollectionExtensions.cs:8-13`, `docs/requirements/Component-SiteCallAudit.md:24-32` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -238,7 +238,7 @@ doc update is tracked separately.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteCallAudit/SiteCallAuditActor.cs:548-563` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs:548-563` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -297,9 +297,9 @@ relay-path crash.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.SiteCallAudit.Tests/SiteCallAuditActorTests.cs:335-392` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.SiteCallAudit.Tests/SiteCallAuditActorTests.cs:335-392` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `SiteCallQueryRequest_StuckOnly_CursorAtNonStuckBoundary_SkipsToNextStuckRow` to `tests/ScadaLink.SiteCallAudit.Tests/SiteCallAuditActorTests.cs` — drives six rows interleaved as `stuck/non-stuck` × 3 (oldest-first), then issues three page-size-1 stuck-only queries. The cursor between each page deliberately lands on a non-stuck row, so the SQL composition of the stuck predicate AND the keyset cursor predicate must skip it. Asserts each page returns exactly one stuck row in DESC-by-CreatedAtUtc order with no overlap and all three stuck rows visited. Locks the invariant that post-filtering does not produce under-filled pages with non-null next cursors.
|
||||
**Resolution (2026-05-28):** Added `SiteCallQueryRequest_StuckOnly_CursorAtNonStuckBoundary_SkipsToNextStuckRow` to `tests/ZB.MOM.WW.ScadaBridge.SiteCallAudit.Tests/SiteCallAuditActorTests.cs` — drives six rows interleaved as `stuck/non-stuck` × 3 (oldest-first), then issues three page-size-1 stuck-only queries. The cursor between each page deliberately lands on a non-stuck row, so the SQL composition of the stuck predicate AND the keyset cursor predicate must skip it. Asserts each page returns exactly one stuck row in DESC-by-CreatedAtUtc order with no overlap and all three stuck rows visited. Locks the invariant that post-filtering does not produce under-filled pages with non-null next cursors.
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.SiteEventLogging` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging` |
|
||||
| Design doc | `docs/requirements/Component-SiteEventLogging.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -110,7 +110,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogPurgeService.cs:100-102`, `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:36-55` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs:100-102`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:36-55` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -150,7 +150,7 @@ reliably observes the database shrinking. Regression test
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogPurgeService.cs:87-105` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs:87-105` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -190,7 +190,7 @@ removed).
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogPurgeService.cs:64,90,100,110,114`, `src/ScadaLink.SiteEventLogging/EventLogQueryService.cs:36`, `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:34,72` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs:64,90,100,110,114`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogQueryService.cs:36`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:34,72` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -233,7 +233,7 @@ concurrently with multiple writer threads.
|
||||
| Original severity | High (re-triaged down to Low on 2026-05-16 — see Re-triage note) |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Won't Fix |
|
||||
| Location | `src/ScadaLink.Host/Actors/AkkaHostedService.cs:313-336`, `src/ScadaLink.SiteEventLogging/EventLogHandlerActor.cs:21-25` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:313-336`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogHandlerActor.cs:21-25` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -277,14 +277,14 @@ as a plain per-node DI singleton (`AddSiteEventLogging`), so it records to a loc
|
||||
SQLite file on **every** node, including the standby. That wastes storage on the
|
||||
standby but does **not** cause the query-returns-nothing symptom the finding
|
||||
describes, because the query singleton always reads the *active* node's (populated)
|
||||
database. Gating the writer to the active node would be a `ScadaLink.Host` wiring
|
||||
database. Gating the writer to the active node would be a `ZB.MOM.WW.ScadaBridge.Host` wiring
|
||||
change, outside this module's scope, and is a minor optimisation rather than a
|
||||
correctness defect.
|
||||
|
||||
Re-triaged from High to Low and closed as **Won't Fix**: the High-severity
|
||||
correctness claim does not hold. Any residual cleanup (gate the standby-node writer;
|
||||
the comment needs no change) can be raised as a fresh Low finding against
|
||||
`ScadaLink.Host` if desired.
|
||||
`ZB.MOM.WW.ScadaBridge.Host` if desired.
|
||||
|
||||
**Resolution**
|
||||
|
||||
@@ -300,7 +300,7 @@ on the active node. No code change made; see the re-triage note above.
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:57-99` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:57-99` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -343,7 +343,7 @@ caller returns in <500 ms while the database is held busy) plus
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:50-52`, `src/ScadaLink.SiteEventLogging/EventLogQueryService.cs:65-81` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:50-52`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogQueryService.cs:65-81` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -380,7 +380,7 @@ tests `Schema_HasIndexOnSeverity` and `SeverityFilteredQuery_UsesIndex_NotFullSc
|
||||
| Severity | Medium (partially re-triaged 2026-05-16 — see Re-triage note) |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogPurgeService.cs:21-30`, `src/ScadaLink.SiteEventLogging/EventLogQueryService.cs:20-28`, `src/ScadaLink.SiteEventLogging/ServiceCollectionExtensions.cs:10-23` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs:21-30`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogQueryService.cs:20-28`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ServiceCollectionExtensions.cs:10-23` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -426,7 +426,7 @@ three services still share one connection/lock. Regression tests
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:92-95` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:92-95` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -463,7 +463,7 @@ Regression test `LogEventAsync_FaultsTask_AndCountsFailure_OnWriteError`.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/ISiteEventLogger.cs:8-10`, `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:57` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ISiteEventLogger.cs:8-10`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:57` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -510,7 +510,7 @@ tests `LogEventAsync_DoesNotBlockCaller_WhenWriteIsSlow` and
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.SiteEventLogging.Tests/` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests/` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -558,7 +558,7 @@ SiteEventLogging-001/-002/-003 were resolved
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/ServiceCollectionExtensions.cs:18-22` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ServiceCollectionExtensions.cs:18-22` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -579,7 +579,7 @@ the misleading comment.
|
||||
|
||||
Resolved 2026-05-16 (commit `pending`): confirmed dead code — a repo-wide search
|
||||
found zero callers of `AddSiteEventLoggingActors`, and `EventLogHandlerActor` is in
|
||||
fact wired up directly in `ScadaLink.Host/Actors/AkkaHostedService.cs` as a cluster
|
||||
fact wired up directly in `ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs` as a cluster
|
||||
singleton (it must be created inside the `ActorSystem` with a resolved
|
||||
`IEventLogQueryService`, which a `IServiceCollection` extension cannot do). The empty
|
||||
placeholder method and its stale "Phase 4+" comment were deleted, and a short
|
||||
@@ -595,7 +595,7 @@ full module suite still passing.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:160-166,193-197` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:160-166,193-197` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -652,7 +652,7 @@ silent-success behaviour and was replaced).
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogQueryService.cs:79-83` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogQueryService.cs:79-83` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -693,7 +693,7 @@ keyword is matched as a literal substring as the design intends. Regression test
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Won't Fix |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogPurgeService.cs:34-48` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs:34-48` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -754,7 +754,7 @@ background scheduler).
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:58-63` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:58-63` |
|
||||
|
||||
**Resolution (2026-05-28):** Switched `SiteEventLogger._writeQueue` to
|
||||
`Channel.CreateBounded<PendingEvent>` with `FullMode = BoundedChannelFullMode.DropOldest`.
|
||||
@@ -777,7 +777,7 @@ from an alarm storm / script failure loop — drives the queue arbitrarily large
|
||||
Every queued `PendingEvent` retains its `TaskCompletionSource` and its payload
|
||||
strings, so there is no upper bound on how much memory the recorder can hold.
|
||||
|
||||
The sister centralized-audit component `ScadaLink.AuditLog/Site/SqliteAuditWriter.cs`
|
||||
The sister centralized-audit component `ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs`
|
||||
addresses the same hot-path-writer problem with
|
||||
`Channel.CreateBounded<...>(new BoundedChannelOptions(_options.ChannelCapacity) { ..., FullMode = BoundedChannelFullMode.Wait })`,
|
||||
giving back-pressure to producers. Site event logging picked the riskier choice for
|
||||
@@ -803,7 +803,7 @@ chosen policy on `ISiteEventLogger.LogEventAsync`.
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogQueryService.cs:67-77`, `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:159`, `src/ScadaLink.SiteEventLogging/EventLogPurgeService.cs:72-78` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogQueryService.cs:67-77`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:159`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs:72-78` |
|
||||
|
||||
**Resolution** — `EventLogQueryService.ExecuteQuery` now calls `.ToUniversalTime()` on `request.From`/`request.To` before `ToString("o")`, so the produced ISO 8601 string always ends in `+00:00` and lexicographically matches the UTC timestamps written by `SiteEventLogger`. `EventLogPurgeService.PurgeByRetention` was also made defensive with an explicit `.ToUniversalTime()` on the cutoff. A regression test (`Query_FiltersByTimeRange_HandlesNonUtcOffset`) constructs a `+05:00` `DateTimeOffset` and asserts the matching UTC-stored events are returned and out-of-range ones are excluded.
|
||||
|
||||
@@ -854,7 +854,7 @@ lexicographic-comparison hazard structurally.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogQueryService.cs:55`, `src/ScadaLink.Commons/Messages/RemoteQuery/EventLogQueryRequest.cs:18` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogQueryService.cs:55`, `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/RemoteQuery/EventLogQueryRequest.cs:18` |
|
||||
|
||||
**Resolution (2026-05-28):** `EventLogQueryService.ExecuteQuery` now clamps
|
||||
`pageSize` to new `SiteEventLogOptions.MaxQueryPageSize` (default 500, matching
|
||||
@@ -902,9 +902,9 @@ refused.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:67-71,225-226` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:67-71,225-226` |
|
||||
|
||||
**Resolution (2026-05-28):** Took option (a)-via-(b) from the recommendation. Softened the `SiteEventLogger.FailedWriteCount` XML doc to describe the actual state ("Available for future Health Monitoring integration — the counter is correct and observable, but the central health-metric pipeline does not yet poll it"), removing the misleading "Health Monitoring can detect a logging outage" claim. The Health Monitoring wiring is left as a tracked follow-up (it requires a `ScadaLink.HealthMonitoring` source change that belongs in a different batch). Promoted `FailedWriteCount { get; }` onto `ISiteEventLogger` so the eventual Health consumer reads it through the interface without a concrete-type downcast. No behaviour change — pure documentation + interface-surface tidy-up; `SiteEventLogger` already exposed the property publicly, and no test fakes/mocks of `ISiteEventLogger` exist in the repo (grep confirms only `SiteEventLogger` implements it), so the interface addition is non-breaking. Existing 59 SiteEventLogging tests remain green.
|
||||
**Resolution (2026-05-28):** Took option (a)-via-(b) from the recommendation. Softened the `SiteEventLogger.FailedWriteCount` XML doc to describe the actual state ("Available for future Health Monitoring integration — the counter is correct and observable, but the central health-metric pipeline does not yet poll it"), removing the misleading "Health Monitoring can detect a logging outage" claim. The Health Monitoring wiring is left as a tracked follow-up (it requires a `ZB.MOM.WW.ScadaBridge.HealthMonitoring` source change that belongs in a different batch). Promoted `FailedWriteCount { get; }` onto `ISiteEventLogger` so the eventual Health consumer reads it through the interface without a concrete-type downcast. No behaviour change — pure documentation + interface-surface tidy-up; `SiteEventLogger` already exposed the property publicly, and no test fakes/mocks of `ISiteEventLogger` exist in the repo (grep confirms only `SiteEventLogger` implements it), so the interface addition is non-breaking. Existing 59 SiteEventLogging tests remain green.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -913,7 +913,7 @@ XML doc statement "Surfaced so Health Monitoring can detect a logging outage
|
||||
instead of relying on a local log line nobody is watching." The implementation is
|
||||
correct (`Interlocked.Increment` on write failure, `Interlocked.Read` getter), but
|
||||
a repo-wide search shows **no** caller anywhere in `src/` reads the property —
|
||||
neither `ScadaLink.HealthMonitoring`, the central health collector, nor the host's
|
||||
neither `ZB.MOM.WW.ScadaBridge.HealthMonitoring`, the central health collector, nor the host's
|
||||
`/health` endpoint. The metric is dead-letter: a logging outage still goes
|
||||
unnoticed in production, contradicting the original finding's resolution claim.
|
||||
|
||||
@@ -938,7 +938,7 @@ file a tracking item for the wiring. The current doc claim is misleading.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogPurgeService.cs:57-95`, `src/ScadaLink.SiteEventLogging/ServiceCollectionExtensions.cs:30-39`, `docs/requirements/Component-SiteEventLogging.md:45` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs:57-95`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ServiceCollectionExtensions.cs:30-39`, `docs/requirements/Component-SiteEventLogging.md:45` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -989,7 +989,7 @@ SiteEventLogging.Tests).
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:144-156`, `src/ScadaLink.SiteEventLogging/ISiteEventLogger.cs:14-15` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:144-156`, `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ISiteEventLogger.cs:14-15` |
|
||||
|
||||
**Resolution (2026-05-28):** `LogEventAsync` now validates `severity` against
|
||||
the closed set `{Info, Warning, Error}` (case-sensitive, matches SQLite
|
||||
@@ -1033,7 +1033,7 @@ case-insensitive. Update the XML doc to match the enforced contract.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/EventLogQueryService.cs:138` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogQueryService.cs:138` |
|
||||
|
||||
**Resolution (2026-05-28):** `EventLogQueryService.ExecuteQuery` now parses the
|
||||
stored ISO 8601 timestamp with `CultureInfo.InvariantCulture` plus
|
||||
@@ -1077,14 +1077,14 @@ avoid all string-parsing.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteEventLogging/SiteEventLogger.cs:52` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogger.cs:52` |
|
||||
|
||||
**Resolution (2026-05-28):** Dropped `Cache=Shared` from the default connection
|
||||
string in `SiteEventLogger`'s ctor — the logger owns exactly one
|
||||
`SqliteConnection` serialised under `_writeLock`, so the cross-connection
|
||||
shared-cache mode is dormant. The `connectionStringOverride` ctor parameter
|
||||
still lets a test path inject `Cache=Shared` explicitly when needed. No test
|
||||
relied on the default option (grep across `tests/ScadaLink.SiteEventLogging.Tests`
|
||||
relied on the default option (grep across `tests/ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests`
|
||||
finds zero references). Behaviour unchanged for production callers.
|
||||
|
||||
**Description**
|
||||
@@ -1118,7 +1118,7 @@ and locking_mode review that should accompany it.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.SiteEventLogging.Tests/EventLogPurgeServiceTests.cs:282-308` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests/EventLogPurgeServiceTests.cs:282-308` |
|
||||
|
||||
**Resolution (2026-05-28):** Promoted the test-local `bool stop` to a `volatile bool _stop` field on `EventLogPurgeServiceTests` (with a doc-comment cross-referencing this finding). Every writer thread now observes the main thread's `_stop = true` flip without relying on JIT/runtime quirks across the `await _eventLogger.LogEventAsync` boundary, so the regression test for SiteEventLogging-003 can no longer hang past xUnit's per-test timeout in release builds. `CancellationTokenSource` was considered (canonical pattern) but `volatile bool` is the minimal-diff fix consistent with the existing structure.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.SiteRuntime` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime` |
|
||||
| Design doc | `docs/requirements/Component-SiteRuntime.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -31,7 +31,7 @@ actor, and the repositories are untested.
|
||||
#### Re-review 2026-05-17 (commit `39d737e`)
|
||||
|
||||
The module was re-reviewed at commit `39d737e`. No source under
|
||||
`src/ScadaLink.SiteRuntime` has changed since the previous review at `9c60592`
|
||||
`src/ZB.MOM.WW.ScadaBridge.SiteRuntime` has changed since the previous review at `9c60592`
|
||||
(the only intervening commits are code-review documentation updates), so all of
|
||||
SiteRuntime-001..013, 015, 016 remain Resolved and SiteRuntime-014 remains
|
||||
Deferred — its Deferred justification (a trigger-evaluation concurrency design
|
||||
@@ -116,7 +116,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs:106`, `src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs:204` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs:106`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:204` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -165,7 +165,7 @@ synchronously.
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs:632` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs:632` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -202,7 +202,7 @@ optimistic `true`.
|
||||
| Severity | High |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs:222` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs:222` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -243,7 +243,7 @@ longer drifts (this additionally addresses the root cause behind SiteRuntime-004
|
||||
| Severity | Medium — re-triaged: already fixed by the SiteRuntime-003 resolution. |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs` (`ApplyDeployment`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs` (`ApplyDeployment`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -283,7 +283,7 @@ Resolved.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs` (`ApplyDeployment`, `HandleDeployPersistenceResult`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs` (`ApplyDeployment`, `HandleDeployPersistenceResult`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -322,7 +322,7 @@ and `Deploy_Success_ReportsSuccessAndPersistsConfig`.
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Repositories/SiteExternalSystemRepository.cs`, `src/ScadaLink.SiteRuntime/Repositories/SiteNotificationRepository.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteNotificationRepository.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -362,7 +362,7 @@ the repository's connection path end-to-end.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Repositories/SiteExternalSystemRepository.cs`, `src/ScadaLink.SiteRuntime/Repositories/SiteNotificationRepository.cs`, `src/ScadaLink.SiteRuntime/Repositories/SyntheticId.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteNotificationRepository.cs`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SyntheticId.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -404,7 +404,7 @@ simulate a process restart and confirm by-ID lookups still resolve.
|
||||
| Severity | Medium |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs` (`HandleStartupConfigsLoaded`, `LoadSharedScriptsFromStorage`, `HandleSharedScriptsLoaded`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs` (`HandleStartupConfigsLoaded`, `LoadSharedScriptsFromStorage`, `HandleSharedScriptsLoaded`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -448,7 +448,7 @@ present).
|
||||
| Severity | Medium |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs`, `src/ScadaLink.SiteRuntime/Actors/AlarmExecutionActor.cs`, `src/ScadaLink.SiteRuntime/Scripts/ScriptExecutionScheduler.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptExecutionActor.cs`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmExecutionActor.cs`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptExecutionScheduler.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -492,7 +492,7 @@ dedicated dispatcher" comments were removed. Regression tests:
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs` (`EnsureDclConnections`, `ComputeConnectionConfigHash`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs` (`EnsureDclConnections`, `ComputeConnectionConfigHash`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -531,7 +531,7 @@ and `EnsureDclConnections_UnchangedConfig_DoesNotReissueCreateCommand`.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Scripts/ScriptCompilationService.cs` (`ValidateTrustModel`) |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptCompilationService.cs` (`ValidateTrustModel`) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -586,7 +586,7 @@ literal/identifier non-detection, allowed-exception resolution); all 39 existing
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Scripts/ScopeAccessors.cs:28` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs:28` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -627,7 +627,7 @@ variants. No behavioural change — this is a documentation finding; existing
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs:414` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:414` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -668,7 +668,7 @@ behaviour to regression-test, so no new test was added; the existing
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Deferred |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs:219`, `src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs:389` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptActor.cs:219`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs:389` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -719,7 +719,7 @@ an out-of-scope or messaging-contract-changing fix.
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs:746` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs:746` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -759,7 +759,7 @@ pass after the fix.
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.SiteRuntime.Tests/` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -805,7 +805,7 @@ green.
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs:625`, `src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs:675`, `src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs:83`, `src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs:93` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:625`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:675`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptActor.cs:83`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs:93` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -866,7 +866,7 @@ test project).
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/ScriptExecutionActor.cs:17` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptExecutionActor.cs:17` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -907,7 +907,7 @@ regression test was added; the existing suite continues to pass.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs:106`, `src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs:113` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:106`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:113` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -955,7 +955,7 @@ Instance Actor produces no `InstanceLifecycleResponse` for either command
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs:285`, `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs:971` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs:285`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs:971` |
|
||||
|
||||
**Resolution** — added a name → terminating-actor-ref shadow
|
||||
(`_terminatingActorsByName`) populated when `HandleDeploy` stops the
|
||||
@@ -1039,7 +1039,7 @@ be gated on "no instance with this name is currently terminating".
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/DeploymentManagerActor.cs:931` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs:931` |
|
||||
|
||||
**Resolution (2026-05-28):** Took the "refactor `EnsureDclConnections` into a shared field-based helper" path. Extracted a new `EnsureDclConnection(name, protocol, primaryJson, backupJson, failoverRetryCount)` method that owns the hash-cache check and the `CreateConnectionCommand` Tell — both the existing inline `EnsureDclConnections(configJson)` and the new artifact path now drive through it. `ComputeConnectionConfigHash` got a field-based overload so the artifact path (which carries data directly on `DataConnectionArtifact`) reuses the same hash logic as the `ConnectionConfig`-based inline path. To keep `_createdConnections` mutation actor-thread-confined (the artifact-deploy persistence runs inside a `Task.Run`), the off-thread persistence dispatches a new internal `ApplyArtifactDataConnectionsToDcl` message back to `Self` after the SQLite writes; the actor-thread handler then iterates and invokes `EnsureDclConnection`. The DCL only sees `CreateConnectionCommand` (no `Update`/`Delete` messages exist in the codebase, and `CreateConnectionCommand` is treated as upsert-by-name — same shape as the inline-config path). Build clean; 302 SiteRuntime tests green (the existing `EnsureDclConnections_ConnectionConfigChanged_ReissuesCreateCommand` regression test still passes through the refactored shared helper).
|
||||
|
||||
@@ -1091,7 +1091,7 @@ and artifact paths can drive through it.
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Scripts/AuditingDbCommand.cs:138` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/AuditingDbCommand.cs:138` |
|
||||
|
||||
**Resolution (2026-05-28):** Took the recommended "expose a proper API surface" path (the SiteRuntime-006 precedent). Added an `internal DbConnection Inner => _inner;` accessor to `AuditingDbConnection`; both classes are `internal sealed` in the same assembly, so the accessor stays out of the public API. The `AuditingDbCommand.DbConnection` setter now unwraps an `AuditingDbConnection` via `auditing.Inner` instead of `Type.GetField("_inner", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(...)`. No reflection, no `!.` null-forgiveness hiding a runtime crash, no static-analyzer/IL-trim noise — and the same module that enforces "no `System.Reflection` in scripts" no longer reflects internally. The getter's `_wrappingConnection ?? _inner.Connection` fallback was left as-is; addressing the `CreateDbCommand()` round-trip concern is a separate behavioural decision (the finding marked it secondary). Build clean; 302 SiteRuntime tests green.
|
||||
|
||||
@@ -1154,7 +1154,7 @@ the parent connection inside `AuditingDbConnection.CreateDbCommand`).
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs:446`, `src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs:340`, `src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs:356`, `src/ScadaLink.SiteRuntime/Actors/AlarmActor.cs:444` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptActor.cs:446`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs:340`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs:356`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs:444` |
|
||||
|
||||
**Resolution (2026-05-28):** All four call sites
|
||||
(`ScriptActor.EvaluateCondition`, `AlarmActor.EvaluateRangeViolation`,
|
||||
@@ -1209,7 +1209,7 @@ on the host's regional settings.
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Tracking/OperationTrackingStore.cs:39`, `src/ScadaLink.SiteRuntime/Tracking/OperationTrackingStore.cs:360` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingStore.cs:39`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingStore.cs:360` |
|
||||
|
||||
**Resolution** — split reads from writes: the single owned
|
||||
`_writeConnection` + `_writeGate` still serialises writers, but
|
||||
@@ -1270,7 +1270,7 @@ into a sync path that calls `_connection.Dispose()` directly.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs:223`, `src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs:246` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:223`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs:246` |
|
||||
|
||||
**Resolution (2026-05-28):** `HandleSetStaticAttribute` now rejects writes
|
||||
whose `command.AttributeName` does not resolve against
|
||||
@@ -1334,7 +1334,7 @@ failure response).
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:10`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:13`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:15`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:17`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:19`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:25`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:28`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:30`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:32`, `src/ScadaLink.SiteRuntime/Messages/ReplicationMessages.cs:34` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:10`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:13`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:15`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:17`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:19`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:25`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:28`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:30`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:32`, `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Messages/ReplicationMessages.cs:34` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1355,7 +1355,7 @@ relied on by the `fixdocs` workflow will flag every record as missing docs.
|
||||
Add a `<summary>` per record naming the direction (outbound → peer / inbound
|
||||
from peer) and what the operation replicates, and `<param>` docs for each
|
||||
record parameter. Mirror the precedent in
|
||||
`src/ScadaLink.Commons/Messages/.../*.cs`. While there, consider sealing the
|
||||
`src/ZB.MOM.WW.ScadaBridge.Commons/Messages/.../*.cs`. While there, consider sealing the
|
||||
inbound vs outbound split with a marker base type (currently they're just
|
||||
named conventionally) so `Receive<ReplicateXxx>` vs `Receive<ApplyXxx>` is
|
||||
expressed at the type level — but that's optional and out of scope for a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.StoreAndForward` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward` |
|
||||
| Design doc | `docs/requirements/Component-StoreAndForward.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -89,7 +89,7 @@ concurrent discard / sweep delete), re-introducing the StoreAndForward-016 stand
|
||||
divergence in that corner. `StoreAndForward-021` (Medium) is a design-doc-vs-code drift
|
||||
that should be reconciled in the doc: the **operation tracking table** is documented
|
||||
inside Component-StoreAndForward.md as a S&F responsibility (lines 21, 49, 77–87, 108,
|
||||
114), but the actual `OperationTrackingStore` lives in `src/ScadaLink.SiteRuntime/
|
||||
114), but the actual `OperationTrackingStore` lives in `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/
|
||||
Tracking/` and is not consumed by S&F at all — the brief's own note flags this. The
|
||||
design doc should be updated to point at SiteRuntime, or the store moved to
|
||||
StoreAndForward.
|
||||
@@ -122,7 +122,7 @@ shutdown sequence (the DI container) can then NRE through the still-running swee
|
||||
| 6 | Performance & resource management | ☑ | No new findings — the connection-per-call documented trade-off and pooled `OpenAsync` remain acceptable. |
|
||||
| 7 | Design-document adherence | ☑ | Operation Tracking Table documented in StoreAndForward but actually lives in SiteRuntime (021); notification non-parking guarantee broken by 018 + 019. |
|
||||
| 8 | Code organization & conventions | ☑ | `IStoreAndForwardSiteContext` silently defaults `SiteId` to empty (023) — a configuration hole rather than an entity placement issue. |
|
||||
| 9 | Testing coverage | ☑ | The seven new findings have no regression tests in `tests/ScadaLink.StoreAndForward.Tests/` — particularly the notification-doesn't-park invariant (018, 019), the requeue-after-reload-null replication gap (020), and the stop-during-sweep behaviour (024). |
|
||||
| 9 | Testing coverage | ☑ | The seven new findings have no regression tests in `tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/` — particularly the notification-doesn't-park invariant (018, 019), the requeue-after-reload-null replication gap (020), and the stop-during-sweep behaviour (024). |
|
||||
| 10 | Documentation & comments | ☑ | `CachedCallAttemptOutcome.ParkedMaxRetries` XML doc says "S&F semantics" but the code applies it to notifications too if 018/019 fire — minor drift, captured under 018. The `TrackedOperationId.TryParse` silent-skip behaviour in `NotifyCachedCallObserverAsync` is documented in the source but not on the public observer contract (022). |
|
||||
|
||||
## Checklist coverage
|
||||
@@ -149,7 +149,7 @@ shutdown sequence (the DI container) can then NRE through the still-running swee
|
||||
| Severity | Critical |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/ReplicationService.cs:40`, `:53`, `:66`; `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:155`, `:212`, `:222`, `:236` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ReplicationService.cs:40`, `:53`, `:66`; `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:155`, `:212`, `:222`, `:236` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -193,7 +193,7 @@ commit whose message references `StoreAndForward-001`.
|
||||
| Original severity | High (re-triaged down to Low on 2026-05-16 — see Re-triage note) |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Deferred |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:162`, `:201` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:162`, `:201` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -221,7 +221,7 @@ transient condition with bounded logging rather than a permanent no-op.
|
||||
|
||||
The finding's central factual claim — *"No caller in the codebase ever calls
|
||||
`RegisterDeliveryHandler`"* and therefore *"every buffered message lands in this dead
|
||||
state"* — is **no longer true at the reviewed code**. `ScadaLink.Host`
|
||||
state"* — is **no longer true at the reviewed code**. `ZB.MOM.WW.ScadaBridge.Host`
|
||||
(`AkkaHostedService.RegisterSiteActors`, `AkkaHostedService.cs:353-379`) registers all
|
||||
three delivery handlers (`ExternalSystem`, `CachedDbWrite`, `Notification`) at site
|
||||
startup, immediately after `StoreAndForwardService.StartAsync()`. The finding was
|
||||
@@ -249,7 +249,7 @@ should be made deliberately rather than forced here.
|
||||
|
||||
_Deferred 2026-05-16 (re-triaged High → Low). Verified again against the source in this
|
||||
pass: the finding's premise (no `RegisterDeliveryHandler` caller anywhere) is stale —
|
||||
`ScadaLink.Host` wires all three handlers at site startup — so the High-severity
|
||||
`ZB.MOM.WW.ScadaBridge.Host` wires all three handlers at site startup — so the High-severity
|
||||
"engine cannot deliver anything" outcome no longer occurs. The residual gap (a message
|
||||
enqueued for a category that genuinely has no handler is buffered then skipped forever)
|
||||
is real but minor. The prescribed fix — making `EnqueueAsync` reject when no handler is
|
||||
@@ -266,7 +266,7 @@ pending that decision rather than forced here._
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:153`, `:229`, `:233` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:153`, `:229`, `:233` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -316,7 +316,7 @@ corrected semantics.
|
||||
| Severity | Medium |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:38`, `:60` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:38`, `:60` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -355,7 +355,7 @@ Documentation-only change — no behavioural code touched, so no regression test
|
||||
| Original severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:184`, `:266`, `:280` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:184`, `:266`, `:280` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -423,7 +423,7 @@ other writer's `RetryCount`).
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardStorage.cs:166`, `:175` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs:166`, `:175` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -466,7 +466,7 @@ finding's recommendation.
|
||||
| Severity | Low |
|
||||
| Category | Akka.NET conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/ParkedMessageHandlerActor.cs:34`, `:68`, `:87` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ParkedMessageHandlerActor.cs:34`, `:68`, `:87` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -511,7 +511,7 @@ is a concrete non-virtual type with no failure-injection seam.
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardStorage.cs:28`, `:61`, `:93`, `:117`, `:144`, `:162`, `:199`, `:221`, `:237`, `:267`, `:285`, `:305`, `:319` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs:28`, `:61`, `:93`, `:117`, `:144`, `:162`, `:199`, `:221`, `:237`, `:267`, `:285`, `:305`, `:319` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -550,7 +550,7 @@ touched, so no regression test (the connection-pool reliance is not test-observa
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:46`, `:309` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:46`, `:309` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -596,7 +596,7 @@ with `WasBuffered == false` and an empty buffer) and
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardStorage.cs:203`, `:101` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs:203`, `:101` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -637,7 +637,7 @@ cleared, message excluded from the retry-due set) and passes post-fix.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Deferred |
|
||||
| Location | `src/ScadaLink.Commons/Types/Enums/StoreAndForwardMessageStatus.cs:9`; `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:219`, `:235` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/StoreAndForwardMessageStatus.cs:9`; `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:219`, `:235` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -663,8 +663,8 @@ StoreAndForward module only ever assigns `Pending` and `Parked`, and `InFlight`
|
||||
`Delivered` are never assigned anywhere (delivered messages are deleted, not marked).
|
||||
The design doc's `retrying` state is unmodelled. Both options the recommendation offers
|
||||
— (a) drop the unused `InFlight`/`Delivered` members, or (b) add a `Retrying` member —
|
||||
require editing `StoreAndForwardMessageStatus.cs`, which lives in `src/ScadaLink.Commons`
|
||||
(outside this review's edit scope: only `src/ScadaLink.StoreAndForward/**` may be
|
||||
require editing `StoreAndForwardMessageStatus.cs`, which lives in `src/ZB.MOM.WW.ScadaBridge.Commons`
|
||||
(outside this review's edit scope: only `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/**` may be
|
||||
changed). The enum is also referenced by IntegrationTests and HealthMonitoring tests, so
|
||||
removing members is a cross-module change. The defect is real but cannot be resolved
|
||||
in-module; **Deferred** to a change that owns the Commons enum and the design doc
|
||||
@@ -677,7 +677,7 @@ together._
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Deferred |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardMessage.cs:9` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardMessage.cs:9` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -706,8 +706,8 @@ mapping to `sf_messages` and is also carried across Akka remoting inside
|
||||
component assembly rather than the Commons `Entities`/`Messages` hierarchy. The
|
||||
recommendation's primary remedy — moving `StoreAndForwardMessage` (and
|
||||
`ReplicationOperation`) into Commons — crosses module boundaries (it would add a type to
|
||||
`src/ScadaLink.Commons`, outside this review's edit scope of
|
||||
`src/ScadaLink.StoreAndForward/**`, and change every referencing module). The alternative
|
||||
`src/ZB.MOM.WW.ScadaBridge.Commons`, outside this review's edit scope of
|
||||
`src/ZB.MOM.WW.ScadaBridge.StoreAndForward/**`, and change every referencing module). The alternative
|
||||
"separate replication DTO" still leaves the persistence entity in the component, so it
|
||||
does not actually resolve the finding's core concern (entity placement / contract-
|
||||
evolution governance). Resolving this is a deliberate code-organisation decision that
|
||||
@@ -721,7 +721,7 @@ cross-module follow-up._
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.StoreAndForward.Tests/` (whole directory); `src/ScadaLink.StoreAndForward/StoreAndForwardStorage.cs:101`; `src/ScadaLink.StoreAndForward/ParkedMessageHandlerActor.cs` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/` (whole directory); `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs:101`; `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ParkedMessageHandlerActor.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -768,7 +768,7 @@ These are coverage-gap tests over already-correct code, so they pass on first ru
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardStorage.cs:26` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardStorage.cs:26` |
|
||||
|
||||
**Found 2026-05-16** while verifying the store-and-forward fixes — this defect was
|
||||
not part of the original baseline review.
|
||||
@@ -806,7 +806,7 @@ all six `SiteActorPathTests` now pass. Fixed by the commit whose message referen
|
||||
| Severity | Medium |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:114`–`:130`, `:285` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:114`–`:130`, `:285` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -873,7 +873,7 @@ test-observable so no regression test was added.
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:339`–`:362`; `src/ScadaLink.StoreAndForward/ReplicationService.cs:131`–`:136` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:339`–`:362`; `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ReplicationService.cs:131`–`:136` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -942,7 +942,7 @@ requeued row and calls `_replication?.ReplicateRequeue`, and the standby's
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:344`, `:358` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:344`, `:358` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -992,7 +992,7 @@ the StoreAndForward-016 replication) — and pass it to `RaiseActivity` (falling
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/NotificationForwarder.cs:62`–`:69`, `:105`–`:122`; `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:369`–`:397` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/NotificationForwarder.cs:62`–`:69`, `:105`–`:122`; `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:369`–`:397` |
|
||||
|
||||
**Resolution** — `NotificationForwarder.DeliverAsync` now discards a corrupt
|
||||
buffered payload instead of returning false. The corrupt path logs a Warning
|
||||
@@ -1053,7 +1053,7 @@ parking exception specifically for notifications, and revise the resolved
|
||||
StoreAndForward-017 wording; (b) treat `JsonException` as transient (would retry-forever
|
||||
on a corrupt payload — bad); (c) introduce a per-category park-allowed flag on the
|
||||
engine and gate the retry-path park behind it for the Notification category.
|
||||
Add a regression test in `tests/ScadaLink.StoreAndForward.Tests/NotificationForwarderTests.
|
||||
Add a regression test in `tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/NotificationForwarderTests.
|
||||
cs` asserting that a corrupt-payload notification reaches a terminal **non-Parked**
|
||||
state — today the corrupt-payload behaviour is uncovered.
|
||||
|
||||
@@ -1068,7 +1068,7 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:229`, `:407`–`:437`; `src/ScadaLink.StoreAndForward/StoreAndForwardOptions.cs:18`; `src/ScadaLink.SiteRuntime/Scripts/ScriptRuntimeContext.cs:1773`–`:1778`; `src/ScadaLink.NotificationService/NotificationDeliveryService.cs:149`–`:156` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:229`, `:407`–`:437`; `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs:18`; `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs:1773`–`:1778`; `src/ZB.MOM.WW.ScadaBridge.NotificationService/NotificationDeliveryService.cs:149`–`:156` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1112,7 +1112,7 @@ parking violation under the engine's normal max-retries policy.
|
||||
|
||||
Make the notification enqueue paths pass `maxRetries: 0` so the documented "no limit /
|
||||
never parked" semantics apply, and guard against regression by adding an integration
|
||||
test in `tests/ScadaLink.StoreAndForward.Tests/NotificationForwarderTests.cs` that runs
|
||||
test in `tests/ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests/NotificationForwarderTests.cs` that runs
|
||||
a sweep many more times than `DefaultMaxRetries` against an always-failing handler and
|
||||
asserts the buffered notification's status stays `Pending` (not `Parked`). A cleaner
|
||||
alternative is to special-case the `Notification` category inside
|
||||
@@ -1135,7 +1135,7 @@ applies uniformly (including to notifications) and that `maxRetries: 0` is the e
|
||||
escape hatch for callers that need unbounded retry. Added a `StoreAndForward-019` block
|
||||
to `StoreAndForwardOptions.DefaultMaxRetries`'s XML doc explaining the same invariant.
|
||||
No behavioural code change — existing tests (104 in
|
||||
`ScadaLink.StoreAndForward.Tests`) continue to pass.
|
||||
`ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests`) continue to pass.
|
||||
|
||||
### StoreAndForward-020 — `RetryParkedMessageAsync` skips standby replication when the message is deleted between local update and re-load
|
||||
|
||||
@@ -1144,7 +1144,7 @@ No behavioural code change — existing tests (104 in
|
||||
| Severity | Medium |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:599`–`:616` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:599`–`:616` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1239,7 +1239,7 @@ the activity log uses the captured `Category` directly.
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `docs/requirements/Component-StoreAndForward.md:21`, `:49`–`:51`, `:77`–`:87`, `:108`, `:114`; `src/ScadaLink.SiteRuntime/Tracking/OperationTrackingStore.cs:37`; `src/ScadaLink.StoreAndForward/` (whole module) |
|
||||
| Location | `docs/requirements/Component-StoreAndForward.md:21`, `:49`–`:51`, `:77`–`:87`, `:108`, `:114`; `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingStore.cs:37`; `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/` (whole module) |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1257,12 +1257,12 @@ this component:
|
||||
each site node holds a **site-local operation tracking table** in SQLite. … Each row
|
||||
records the operation kind (`TrackedOperationKind`) …"
|
||||
|
||||
The actual implementation lives outside this module: `src/ScadaLink.SiteRuntime/
|
||||
The actual implementation lives outside this module: `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/
|
||||
Tracking/OperationTrackingStore.cs` (and `IOperationTrackingStore`, `OperationTrackingOptions`).
|
||||
The StoreAndForward project contains no references to the tracking store, owns no
|
||||
`operation_tracking` table, and `StoreAndForwardService.NotifyCachedCallObserverAsync`
|
||||
is only a hook handing telemetry context to an `ICachedCallLifecycleObserver` — the
|
||||
audit bridge wired in `ScadaLink.AuditLog`. The S&F module is **not** the table's
|
||||
audit bridge wired in `ZB.MOM.WW.ScadaBridge.AuditLog`. The S&F module is **not** the table's
|
||||
owner; SiteRuntime is.
|
||||
|
||||
This is a real design-doc drift, not a code defect, and is flagged explicitly in the
|
||||
@@ -1271,7 +1271,7 @@ discussion of the lifecycle — "immediate success writes a terminal Delivered t
|
||||
row directly here", "operator discard sets terminal `Discarded`", "central never
|
||||
mutates the mirror row directly" — places coordination responsibilities on the wrong
|
||||
component. A reader looking for the source of truth for `Tracking.Status(id)` would
|
||||
read `Component-StoreAndForward.md` and search `src/ScadaLink.StoreAndForward/` in
|
||||
read `Component-StoreAndForward.md` and search `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/` in
|
||||
vain. The doc also lists Site Call Audit / Audit Log telemetry-emission as a S&F
|
||||
responsibility (line 22), but the emission actually happens via the `AuditLog` site
|
||||
component subscribing to `ICachedCallLifecycleObserver`.
|
||||
@@ -1313,7 +1313,7 @@ not owned here.
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:484`–`:515` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:484`–`:515` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1332,7 +1332,7 @@ TrackedOperationId threaded in)", but the documented contract is broken in two w
|
||||
so a misconfigured caller bypasses the audit hot path with zero feedback.
|
||||
|
||||
2. **The contract is hidden in field-level XML.** The `ICachedCallLifecycleObserver`
|
||||
public interface contract (defined in `ScadaLink.Commons`) does not document that
|
||||
public interface contract (defined in `ZB.MOM.WW.ScadaBridge.Commons`) does not document that
|
||||
the observer will be silently skipped when the underlying S&F message id is not a
|
||||
GUID. A consumer reading the interface contract reasonably expects every cached-call
|
||||
attempt to surface — the audit pipeline depends on it. The silent-drop is an
|
||||
@@ -1377,7 +1377,7 @@ is on `_observer.Notifications` being empty, which is unchanged.
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/ServiceCollectionExtensions.cs:43`–`:53`; `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:99`, `:524` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ServiceCollectionExtensions.cs:43`–`:53`; `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:99`, `:524` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1436,7 +1436,7 @@ normalisation is a no-op there.
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.StoreAndForward/StoreAndForwardService.cs:122`–`:127`, `:136`–`:143`, `:303`–`:329` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs:122`–`:127`, `:136`–`:143`, `:303`–`:329` |
|
||||
|
||||
**Resolution (2026-05-28):** the timer callback now captures the sweep task
|
||||
into a `_sweepTask` field via `Volatile.Write`, and `StopAsync` disposes the
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.TemplateEngine` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine` |
|
||||
| Design doc | `docs/requirements/Component-TemplateEngine.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -128,7 +128,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs:211`, `src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs:535`, `src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs:609` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs:211`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs:535`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs:609` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -167,7 +167,7 @@ Regression tests: `Flatten_ThreeLevelComposition_AttributesAlarmsScriptsAllResol
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:799` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:799` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -195,7 +195,7 @@ Resolved 2026-05-16 (commit `<pending>`): implemented the per-slot alarm
|
||||
override mechanism as a coordinated `Commons` + `ConfigurationDatabase` +
|
||||
`TemplateEngine` change, mirroring the existing attribute/script override
|
||||
design. Added `IsInherited` / `LockedInDerived` to the `TemplateAlarm` POCO
|
||||
(`ScadaLink.Commons`) and an EF migration `AddDerivedAlarmFields` adding two
|
||||
(`ZB.MOM.WW.ScadaBridge.Commons`) and an EF migration `AddDerivedAlarmFields` adding two
|
||||
`bit NOT NULL DEFAULT 0` columns to `TemplateAlarms`. `BuildDerivedTemplate`
|
||||
now copies base alarms as `IsInherited = true` placeholder rows.
|
||||
`FlatteningService.ResolveInheritedAlarms` skips `IsInherited` placeholder
|
||||
@@ -218,7 +218,7 @@ now rejects a derived override of a `LockedInDerived` base alarm.
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:285` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:285` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -260,7 +260,7 @@ tests: `UpdateAttribute_UnlockedAttribute_DataTypeChangeRejected`,
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs:695` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs:695` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -300,7 +300,7 @@ all see the reference. Regression tests:
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:56` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:56` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -339,7 +339,7 @@ Regression test: `CreateTemplate_WithParent_DoesNotRunDeadCollisionQuery`.
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Validation/ScriptCompiler.cs:21`, `src/ScadaLink.TemplateEngine/Validation/ValidationService.cs:318` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/ScriptCompiler.cs:21`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/ValidationService.cs:318` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -386,7 +386,7 @@ doc, and the scan is explicitly labelled advisory. Regression tests:
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Validation/ScriptCompiler.cs:54`, `src/ScadaLink.TemplateEngine/SharedScriptService.cs:124` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/ScriptCompiler.cs:54`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/SharedScriptService.cs:124` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -429,7 +429,7 @@ genuine mismatches are still caught. Regression tests in `ScriptCompilerTests`
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Services/InstanceService.cs:178` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs:178` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -468,7 +468,7 @@ lock-bypass. Regression tests: `SetAlarmOverride_NonExistentAlarm_ReturnsFailure
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Services/TemplateDeletionService.cs:75` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateDeletionService.cs:75` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -506,7 +506,7 @@ the `Compositions` navigation, matching how EF's `GetAllTemplatesAsync` loads it
|
||||
| Severity | Low — re-triaged from Medium: this is a stale XML comment, not a behavioural defect. The code matches the design (last-write-wins); only the doc string was wrong. |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Services/InstanceService.cs:9` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/InstanceService.cs:9` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -550,7 +550,7 @@ behaviour change; no regression test (documentation-only).
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Flattening/RevisionHashService.cs:136` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs:136` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -592,7 +592,7 @@ test gate instead of silently changing every revision hash.
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Deferred |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Validation/SemanticValidator.cs:18` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/SemanticValidator.cs:18` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -612,13 +612,13 @@ or rename the enum to match the doc if "Integer" is the intended canonical name.
|
||||
**Re-triage**
|
||||
|
||||
Verified against the source: the `DataType` enum is declared in
|
||||
`src/ScadaLink.Commons/Types/Enums/DataType.cs` (`Boolean, Int32, Float, Double,
|
||||
`src/ZB.MOM.WW.ScadaBridge.Commons/Types/Enums/DataType.cs` (`Boolean, Int32, Float, Double,
|
||||
String, DateTime, Binary`) — **not** in the TemplateEngine module — and is consumed
|
||||
across modules (`TemplateAttribute` entity, management command contracts). The only
|
||||
in-module file the finding cites, `SemanticValidator.cs:18`, is confirmed **correct**:
|
||||
`NumericDataTypes` already hard-codes the real enum names. Both remediation options
|
||||
in the recommendation therefore land **outside** this module's resolution boundary
|
||||
(`src/ScadaLink.TemplateEngine/**`): renaming the enum touches `ScadaLink.Commons`
|
||||
(`src/ZB.MOM.WW.ScadaBridge.TemplateEngine/**`): renaming the enum touches `ZB.MOM.WW.ScadaBridge.Commons`
|
||||
(and every consumer of `DataType`), and the alternative — updating the design doc —
|
||||
touches `docs/requirements/Component-TemplateEngine.md`. There is no in-module code
|
||||
defect to fix. Re-triaged from Open to Deferred: the fix is a one-line design-doc
|
||||
@@ -630,7 +630,7 @@ String") that must be made by an agent owning the docs / Commons scope.
|
||||
Deferred 2026-05-16 (no commit): no in-module fix possible — see Re-triage. The
|
||||
TemplateEngine code is correct as-is. FLAGGED for the docs owner: correct the
|
||||
Attribute data-type list in `docs/requirements/Component-TemplateEngine.md` to match
|
||||
`ScadaLink.Commons` `DataType` (`Boolean, Int32, Float, Double, String, DateTime,
|
||||
`ZB.MOM.WW.ScadaBridge.Commons` `DataType` (`Boolean, Int32, Float, Double, String, DateTime,
|
||||
Binary`). Renaming the enum is not recommended (cross-module churn for no behavioural
|
||||
gain); the doc is the authoritative thing to fix.
|
||||
|
||||
@@ -641,7 +641,7 @@ gain); the doc is the authoritative thing to fix.
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/CycleDetector.cs:30`, `src/ScadaLink.TemplateEngine/CycleDetector.cs:38` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/CycleDetector.cs:30`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/CycleDetector.cs:38` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -688,7 +688,7 @@ are detected. Regression tests:
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:109`, `src/ScadaLink.TemplateEngine/Services/TemplateDeletionService.cs:27` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:109`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Services/TemplateDeletionService.cs:27` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -731,7 +731,7 @@ verifies all three constraint categories are surfaced together.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:680` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:680` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -791,7 +791,7 @@ mutates. Regression tests:
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Flattening/FlatteningService.cs:750` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/FlatteningService.cs:750` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -844,7 +844,7 @@ resolves against the real parent module. Regression test:
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Flattening/RevisionHashService.cs:128`, `src/ScadaLink.TemplateEngine/Flattening/RevisionHashService.cs:156`, `src/ScadaLink.TemplateEngine/Flattening/RevisionHashService.cs:42`, `src/ScadaLink.TemplateEngine/Flattening/DiffService.cs:110`, `src/ScadaLink.TemplateEngine/Flattening/DiffService.cs:118` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs:128`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs:156`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/RevisionHashService.cs:42`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs:110`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs:118` |
|
||||
|
||||
**Resolution** — Added `Description` to `HashableAttribute` and `HashableAlarm` (placed alphabetically per the determinism contract) and introduced a `HashableConnection` projection plus a `SortedDictionary<string, HashableConnection> Connections` field on `HashableConfiguration` that captures protocol, primary/backup JSON, and failover retry count for every deployed connection. `DiffService.AttributesEqual` and `AlarmsEqual` now compare `Description`, and a new public `ConnectionsEqual` helper covers connection-endpoint drift so callers can detect the change in the same shape used by the other entity comparators. Regression tests `ComputeHash_AttributeDescriptionEdit_ChangesHash`, `ComputeHash_AlarmDescriptionEdit_ChangesHash`, `ComputeHash_ConnectionEndpointEdit_ChangesHash`, and `ConnectionsEqual_EndpointEdit_ReturnsFalse` lock the behaviour in.
|
||||
|
||||
@@ -914,9 +914,9 @@ TemplateEngine-018.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/Flattening/DiffService.cs:19` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs:19` |
|
||||
|
||||
**Resolution (2026-05-28):** Added `DiffService.ComputeConnectionsDiff(oldConfig, newConfig)`, which mirrors the existing attribute/alarm/script `ComputeEntityDiff` shape over the `FlattenedConfiguration.Connections` map and emits `DiffEntry<ConnectionConfig>` Added / Removed / Changed entries keyed by connection name (delegating equality to the existing `ConnectionsEqual` helper). Kept the new diff as a parallel method on `DiffService` rather than extending `ConfigurationDiff` (which lives in `ScadaLink.Commons`, outside this fix's scope); the public-record extension and Central UI plumbing are a paired Commons follow-up. Regression tests: `ComputeConnectionsDiff_NewBindingAdded_ReportedAsAdded`, `ComputeConnectionsDiff_BindingCleared_ReportedAsRemoved`, `ComputeConnectionsDiff_EndpointEdit_ReportedAsChanged`, `ComputeConnectionsDiff_IdenticalConnections_NoEntries`.
|
||||
**Resolution (2026-05-28):** Added `DiffService.ComputeConnectionsDiff(oldConfig, newConfig)`, which mirrors the existing attribute/alarm/script `ComputeEntityDiff` shape over the `FlattenedConfiguration.Connections` map and emits `DiffEntry<ConnectionConfig>` Added / Removed / Changed entries keyed by connection name (delegating equality to the existing `ConnectionsEqual` helper). Kept the new diff as a parallel method on `DiffService` rather than extending `ConfigurationDiff` (which lives in `ZB.MOM.WW.ScadaBridge.Commons`, outside this fix's scope); the public-record extension and Central UI plumbing are a paired Commons follow-up. Regression tests: `ComputeConnectionsDiff_NewBindingAdded_ReportedAsAdded`, `ComputeConnectionsDiff_BindingCleared_ReportedAsRemoved`, `ComputeConnectionsDiff_EndpointEdit_ReportedAsChanged`, `ComputeConnectionsDiff_IdenticalConnections_NoEntries`.
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -957,7 +957,7 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateResolver.cs:117`, `src/ScadaLink.TemplateEngine/TemplateResolver.cs:123` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateResolver.cs:117`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateResolver.cs:123` |
|
||||
|
||||
**Resolution (2026-05-28):** `BuildInheritanceChain` now walks the parent chain via the `int?` `ParentTemplateId` directly — only a missing (`null`) value means "no parent", so a real template Id of 0 walks the chain like any other node (matching the duplicate-tolerant `BuildLookup` and the TemplateEngine-013 `CycleDetector` fix). Regression tests: `BuildInheritanceChain_RealIdZero_IsTreatedAsParentReferenceNotAsNoParent`, `BuildInheritanceChain_ParentChainThroughIdZero_DoesNotTruncateChainAtZero`, and the end-to-end `ResolveAllMembers_TemplateWithRealIdZero_StillResolvesItsMembers`.
|
||||
|
||||
@@ -1019,7 +1019,7 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:77`, `src/ScadaLink.TemplateEngine/TemplateService.cs:256`, `src/ScadaLink.TemplateEngine/TemplateService.cs:407`, `src/ScadaLink.TemplateEngine/TemplateService.cs:556`, `src/ScadaLink.TemplateEngine/TemplateService.cs:734`, `src/ScadaLink.TemplateEngine/SharedScriptService.cs:71` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:77`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:256`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:407`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:556`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:734`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/SharedScriptService.cs:71` |
|
||||
|
||||
**Resolution (2026-05-28):** Every `Create*` path in `TemplateService` (`CreateTemplateAsync`, `AddAttributeAsync`, `AddAlarmAsync`, `AddScriptAsync`, `AddCompositionAsync`) and `SharedScriptService.CreateSharedScriptAsync` now follows the `InstanceService.CreateInstanceAsync` shape — save the entity first so EF Core populates the auto-generated key, then log the audit row with the real `entity.Id`, then save the audit row. `AddCompositionAsync` already saved the composition row inside `CreateCascadedCompositionAsync` before returning, so only its `LogAsync` call needed to switch from `"0"` to `composition.Id.ToString()`. Regression tests assert the captured audit `entityId` equals the post-save id (not `"0"`): `CreateTemplate_AuditRowCarriesRealTemplateIdNotLiteralZero`, `AddAttribute_AuditRowCarriesRealAttributeIdNotLiteralZero`, `AddAlarm_AuditRowCarriesRealAlarmIdNotLiteralZero`, `AddScript_AuditRowCarriesRealScriptIdNotLiteralZero`, and `CreateSharedScript_AuditRowCarriesRealScriptIdNotLiteralZero`.
|
||||
|
||||
@@ -1074,7 +1074,7 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/TemplateService.cs:173` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:173` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -1131,7 +1131,7 @@ template in a *different* folder is not a collision).
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.TemplateEngine/LockEnforcer.cs:109`, `src/ScadaLink.TemplateEngine/TemplateService.cs:323`, `src/ScadaLink.TemplateEngine/TemplateService.cs:476`, `src/ScadaLink.TemplateEngine/TemplateService.cs:623` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/LockEnforcer.cs:109`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:323`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:476`, `src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs:623` |
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.Transport` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.Transport` |
|
||||
| Design doc | `docs/requirements/Component-Transport.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-28 |
|
||||
@@ -54,7 +54,7 @@ entry-count / per-entry decompression cap).
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/Import/BundleImporter.cs:844-851` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:844-851` |
|
||||
|
||||
**Resolution** — Extended `ApplyTemplatesAsync`'s Overwrite branch with three
|
||||
new private diff-and-merge helpers (`SyncTemplateAttributesAsync`,
|
||||
@@ -101,7 +101,7 @@ template whose Scripts / Attributes / Alarms differ.
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/Import/BundleImporter.cs:1213-1221` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:1213-1221` |
|
||||
|
||||
**Resolution** — Added a private `SyncExternalSystemMethodsAsync` helper to
|
||||
`BundleImporter` modeled on the T-001 `SyncTemplate*Async` helpers (dictionary-
|
||||
@@ -137,7 +137,7 @@ methods differ.
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/Import/BundleImporter.cs:184-203`, `src/ScadaLink.CentralUI/Components/Pages/Design/TransportImport.razor.cs:267-309`, `src/ScadaLink.Commons/Types/Transport/BundleSession.cs:14-16` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:184-203`, `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor.cs:267-309`, `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSession.cs:14-16` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -174,7 +174,7 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/TransportOptions.cs:12`, `docs/requirements/Component-Transport.md` §11 |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/TransportOptions.cs:12`, `docs/requirements/Component-Transport.md` §11 |
|
||||
|
||||
**Resolution (2026-05-28):** Added a new `BundleUnlockRateLimiter` class (in-memory, per-key sliding-window counter via `ConcurrentDictionary<string, Queue<DateTimeOffset>>` with per-bucket locking), registered as a singleton in `AddTransport`, and wired into `BundleImporter.LoadAsync` before the decrypt attempt — exceeding the per-window cap throws the new `BundleUnlockRateLimitedException` (429-equivalent). The importer keys the limiter on the bundle's `ContentHash` (it has no `IHttpContext` dependency by design); an IP-aware caller can use the limiter's public `TryRegisterAttempt(clientIp, max)` directly for true per-IP keying. `BundleUnlockRateLimiterTests` covers: N attempts allowed and N+1 rejected; full-window expiry releases the entire budget; partial expiry releases only the aged-out slots (sliding window); per-key isolation; argument validation.
|
||||
|
||||
@@ -208,12 +208,12 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/Encryption/BundleSecretEncryptor.cs:31-49`, `src/ScadaLink.Transport/Serialization/ManifestValidator.cs:29-53` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Encryption/BundleSecretEncryptor.cs:31-49`, `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/ManifestValidator.cs:29-53` |
|
||||
|
||||
**Description**
|
||||
|
||||
AES-GCM is called with no Associated Authenticated Data (AAD). The `manifest`
|
||||
fields — `SourceEnvironment`, `ExportedBy`, `ScadaLinkVersion`, `Summary`,
|
||||
fields — `SourceEnvironment`, `ExportedBy`, `ScadaBridgeVersion`, `Summary`,
|
||||
`Contents`, `CreatedAtUtc`, etc. — are plaintext and only the `ContentHash`
|
||||
field is checked against the content bytes. An attacker who obtains a bundle
|
||||
can edit any non-`ContentHash` manifest field (e.g. rewrite the
|
||||
@@ -244,7 +244,7 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/Serialization/BundleSerializer.cs:121-156`, `src/ScadaLink.Transport/Import/BundleImporter.cs:132-143` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/BundleSerializer.cs:121-156`, `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:132-143` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -277,7 +277,7 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/Import/BundleImporter.cs:614-696`, `src/ScadaLink.Transport/Import/BundleSessionStore.cs:67-93` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:614-696`, `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleSessionStore.cs:67-93` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -313,7 +313,7 @@ _Unresolved._
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/Import/BundleImporter.cs:252-272` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:252-272` |
|
||||
|
||||
**Resolution (2026-05-28):** Added a bulk `GetTemplatesWithChildrenAsync(IEnumerable<string> names, CancellationToken)` method on `ITemplateEngineRepository` and implemented it on `TemplateEngineRepository` with a single `Where(t => names.Contains(t.Name))` + `Include(Attributes/Alarms/Scripts/Compositions).AsSplitQuery()` round-trip; null / empty / duplicate names are filtered before the IN clause. `BundleImporter.PreviewAsync` now calls the bulk method once with `content.Templates.Select(t => t.Name)` and indexes the result by name, replacing the previous `GetAllTemplatesAsync` scan + per-match `GetTemplateWithChildrenAsync(stub.Id)` round-trip pair. Regression tests: `TemplateEngineRepositoryTests.GetTemplatesWithChildrenAsync_*` (4 tests — bulk fetch, empty input, null enumerable, dedup) and `BundleImporterPreviewTests.PreviewAsync_multiple_templates_with_children_diffs_each_correctly` (asserts the bulk fetch hydrates all three templates' Attributes / Alarms / Scripts so the diff classifies each as Identical).
|
||||
|
||||
@@ -346,7 +346,7 @@ _Unresolved._
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.Transport/Import/BundleImporter.cs:528, 668, 703`, `src/ScadaLink.ConfigurationDatabase/Services/AuditCorrelationContext.cs` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:528, 668, 703`, `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/AuditCorrelationContext.cs` |
|
||||
|
||||
**Resolution (2026-05-28):**
|
||||
Took option (a). `AuditCorrelationContext` now backs `BundleImportId` with a `static AsyncLocal<Guid?>`,
|
||||
@@ -389,14 +389,14 @@ _Unresolved._
|
||||
| Severity | Medium |
|
||||
| Category | Testing coverage |
|
||||
| Status | Resolved |
|
||||
| Location | `tests/ScadaLink.Transport.IntegrationTests/ConflictResolutionTests.cs`, `tests/ScadaLink.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs` |
|
||||
| Location | `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/ConflictResolutionTests.cs`, `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs` |
|
||||
|
||||
**Resolution (2026-05-28):** Re-verified the listed gaps against the current
|
||||
tree. Each item the finding enumerated has landed in the recent fix commits:
|
||||
|
||||
- **Template Overwrite with divergent Attributes / Alarms / Scripts** — covered
|
||||
by `ApplyAsync_Overwrite_synchronises_attributes_alarms_and_scripts_to_bundle`
|
||||
in `tests/ScadaLink.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs`
|
||||
in `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs`
|
||||
(added with the Transport-001 fix in commit `e3ca9af`). Asserts the bundle's
|
||||
child collections fully replace the divergent target shape AND that per-field
|
||||
audit rows (`TemplateAttributeAdded`/`Updated`/`Deleted`, the alarm and script
|
||||
@@ -406,10 +406,10 @@ tree. Each item the finding enumerated has landed in the recent fix commits:
|
||||
same file (Transport-002 fix, commit `e3ca9af`). Mirrors the T-001 shape with
|
||||
`ExternalSystemMethodAdded`/`Updated`/`Deleted` audit rows.
|
||||
- **Per-IP unlock-throttle behaviour (Transport-004)** — covered by
|
||||
`tests/ScadaLink.Transport.Tests/Import/BundleUnlockRateLimiterTests.cs` (12
|
||||
`tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/BundleUnlockRateLimiterTests.cs` (12
|
||||
tests: under-limit, at-limit rejection, sliding-window reset, per-key isolation).
|
||||
- **Failed-apply session retention (Transport-007)** — covered by
|
||||
`tests/ScadaLink.Transport.Tests/Import/BundleSessionStoreTests.cs`:
|
||||
`tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/BundleSessionStoreTests.cs`:
|
||||
`Get_after_TTL_returns_null_and_evicts`, `EvictExpired_removes_all_past_ttl`,
|
||||
`UnlockFailures_ExpireOnTtlAndGetReturnsZero`, and
|
||||
`UnlockFailures_EvictExpired_ClearsStaleEntries` collectively pin the TTL
|
||||
@@ -460,7 +460,7 @@ Add the missing integration tests above. Most can be modelled after
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `docs/requirements/Component-Transport.md` Import Flow Step 1, `src/ScadaLink.Transport/Import/BundleImporter.cs:124-203` |
|
||||
| Location | `docs/requirements/Component-Transport.md` Import Flow Step 1, `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs:124-203` |
|
||||
|
||||
**Description**
|
||||
|
||||
@@ -500,7 +500,7 @@ bundle prompt is surfaced AFTER the manifest+hash check, and that a dedicated
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Resolved |
|
||||
| Location | `docs/requirements/Component-Transport.md` §Audit Trail, `src/ScadaLink.ConfigurationDatabase/Repositories/CentralUiRepository.cs:148` |
|
||||
| Location | `docs/requirements/Component-Transport.md` §Audit Trail, `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/CentralUiRepository.cs:148` |
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.<Module>` |
|
||||
| Module | `src/ZB.MOM.WW.ScadaBridge.<Module>` |
|
||||
| Design doc | `docs/requirements/Component-<Name>.md` |
|
||||
| Status | Not yet reviewed \| In progress \| Reviewed |
|
||||
| Last reviewed | YYYY-MM-DD |
|
||||
@@ -50,7 +50,7 @@ Confirm every category was examined. Record "No issues found" where applicable.
|
||||
| Severity | Critical \| High \| Medium \| Low |
|
||||
| Category | <one of the 10 checklist categories> |
|
||||
| Status | Open \| In Progress \| Resolved \| Won't Fix \| Deferred |
|
||||
| Location | `src/ScadaLink.<Module>/<File>.cs:<line>` |
|
||||
| Location | `src/ZB.MOM.WW.ScadaBridge.<Module>/<File>.cs:<line>` |
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ def build_readme(modules, per_module):
|
||||
|
||||
add("# Code Reviews")
|
||||
add("")
|
||||
add("Comprehensive, per-module code reviews of the ScadaLink codebase. Each module (one")
|
||||
add("Comprehensive, per-module code reviews of the ScadaBridge codebase. Each module (one")
|
||||
add("buildable project under `src/`) has its own folder containing a `findings.md`. This")
|
||||
add("README is the aggregated index — the single place to see all outstanding work.")
|
||||
add("")
|
||||
|
||||
Reference in New Issue
Block a user