docs(code-reviews): re-review batch 1 at 39d737e — CentralUI, CLI, ClusterInfrastructure, Commons, Communication

17 new findings: CentralUI-020..025, CLI-014..016, ClusterInfrastructure-009..010, Commons-013..014, Communication-012..015.
This commit is contained in:
Joseph Doherty
2026-05-17 00:41:21 -04:00
parent 39d737ebd6
commit e49846603e
6 changed files with 842 additions and 52 deletions

View File

@@ -5,10 +5,10 @@
| Module | `src/ScadaLink.Commons` |
| Design doc | `docs/requirements/Component-Commons.md` |
| Status | Reviewed |
| Last reviewed | 2026-05-16 |
| Last reviewed | 2026-05-17 |
| Reviewer | claude-agent |
| Commit reviewed | `9c60592` |
| Open findings | 0 |
| Commit reviewed | `39d737e` |
| Open findings | 2 |
## Summary
@@ -32,14 +32,28 @@ kind of edge-case logic that warrants them. Entity and message contracts otherwi
clean and additive-evolution-friendly, with the exception of one `ValueTuple` use in a
wire command.
**Re-review 2026-05-17 (commit `39d737e`).** All twelve prior findings (Commons-001
through Commons-012) are confirmed `Resolved` — the fixes are sound, well-targeted, and
backed by focused regression tests (`StaleTagMonitorRaceTests`, `DynamicJsonElementTests`,
`ScriptParametersTests`, `ManagementCommandRegistryTests`, `OpcUaEndpointConfigSerializerTests`,
`ResultTests`, `ValueFormatterTests`, `ConnectionBindingSerializationTests`,
`FlatteningAndScriptScopeTests`). The new files introduced since `9c60592`
(`TemplateAlarm` lock/inherit fields, `IExternalSystemRepository` name-keyed lookups,
`DeploymentStateQueryRequest`/`Response`, `ParameterDefinition`) follow the established
POCO / record / additive-evolution conventions and carry round-trip compatibility tests.
Two new Low-severity findings were recorded this pass: a `DynamicJsonElement` array
indexer that rejects `long` indices (Commons-013) and an `OpcUaEndpointConfigSerializer`
legacy-fallback path that can mislabel a corrupt new-shape row as `Legacy` (Commons-014).
No Critical, High, or Medium issues were found.
## Checklist coverage
| # | Category | Examined | Notes |
|---|----------|----------|-------|
| 1 | Correctness & logic bugs | ✓ | `DynamicJsonElement.TryConvert` returns success for non-convertible types; `Result<T>` allows null error; legacy-config fallback loses data. |
| 1 | Correctness & logic bugs | ✓ | `DynamicJsonElement.TryConvert` returns success for non-convertible types; `Result<T>` allows null error; legacy-config fallback loses data. Re-review: `DynamicJsonElement.TryGetIndex` rejects non-`int` indices (Commons-013). |
| 2 | Akka.NET conventions | ✓ | Commons has no actors (correct). Message contracts are records and immutable. One wire message uses `ValueTuple` (Commons-008). Correlation IDs present on request/response messages. |
| 3 | Concurrency & thread safety | ✓ | `StaleTagMonitor` has a check-then-act race between the timer callback and `OnValueReceived` (Commons-001). |
| 4 | Error handling & resilience | ✓ | `ScriptParameters.GetNullable` silently swallows conversion failures (Commons-003); OPC UA legacy deserialize discards malformed input (Commons-005). |
| 4 | Error handling & resilience | ✓ | `ScriptParameters.GetNullable` silently swallows conversion failures (Commons-003); OPC UA legacy deserialize discards malformed input (Commons-005). Re-review: corrupt typed OPC UA rows can fall through to the legacy path and be mislabelled `Legacy` (Commons-014). |
| 5 | Security | ✓ | No auth logic here. `SmtpConfiguration.Credentials` / OPC UA passwords are plain-string fields (storage/encryption is a consumer concern) — noted, not a finding. No script-trust violations: Commons defines no forbidden-API surface. |
| 6 | Performance & resource management | ✓ | `StaleTagMonitor` disposes its `Timer` correctly. `DynamicJsonElement` references a `JsonElement` whose backing document lifetime is not owned (Commons-002). |
| 7 | Design-document adherence | ✓ | Several behavior-bearing helper/validator/serializer classes push against REQ-COM-6 "no business logic" (Commons-007). Folder layout matches REQ-COM-5b. |
@@ -566,3 +580,75 @@ the parameterless `ToString()`). The XML doc gained a remarks block stating the
culture-invariant contract and why. Regression tests added in `ValueFormatterTests`
(`FormatDisplayValue_Double_UsesInvariantCulture_*`, `_DateTime_*`, `_CollectionOfDoubles_*`,
each pinned under `de-DE`).
### Commons-013 — `DynamicJsonElement.TryGetIndex` rejects non-`int` index values
| | |
|--|--|
| Severity | Low |
| Category | Correctness & logic bugs |
| Status | Open |
| Location | `src/ScadaLink.Commons/Types/DynamicJsonElement.cs:40-54` |
**Description**
`TryGetIndex` accepts an index only when `indexes[0] is int index`. `DynamicJsonElement`
is designed for dynamic access from scripts (`obj.items[0]`). In a `dynamic` expression the
index operand's runtime type follows the script's variable type — a script that computes
an index in a loop counter or reads it from another `DynamicJsonElement` (whose numbers
are unwrapped as `long` by `Wrap`, see `:105`) will pass a `long`, not an `int`. The
pattern match then fails, `TryGetIndex` returns `false`, and the dynamic binder throws a
`RuntimeBinderException` for what is a perfectly valid in-range index. Because the wrapper
itself surfaces JSON numbers as `long`, `obj.items[obj.count - 1]` — count being a wrapped
JSON number — is the exact failing case. The `int`-only guard also silently rejects
`byte`/`short` indices that would widen to a valid array position.
**Recommendation**
Accept any integral index by converting through `Convert.ToInt64` (guarded for
`OverflowException`) or by matching `int`, `long`, `short`, `byte` and normalizing to a
single integer before the bounds check. Add a regression test indexing with a `long`.
**Resolution**
_Unresolved._
### Commons-014 — `OpcUaEndpointConfigSerializer.Deserialize` can mislabel a corrupt typed row as `Legacy`
| | |
|--|--|
| Severity | Low |
| Category | Error handling & resilience |
| Status | Open |
| Location | `src/ScadaLink.Commons/Serialization/OpcUaEndpointConfigSerializer.cs:107-131` |
**Description**
`Deserialize` tries the typed path first: it parses the document, checks for an
`endpointUrl` property, then calls `JsonSerializer.Deserialize<OpcUaEndpointConfig>`.
The whole block is wrapped in `catch (JsonException) { /* fall through to legacy */ }`.
If a row *is* the current typed shape (it has `endpointUrl`) but is corrupt in a way that
makes `JsonSerializer.Deserialize` throw a `JsonException` — e.g. an enum-valued field
holding an unrecognised string, or a numeric field holding a non-numeric token — the
exception is swallowed and control falls through to `LoadLegacy`. `LoadLegacy` only
requires the root to be a JSON object, so it will usually succeed against the same input
and the result is reported as `OpcUaConfigParseStatus.Legacy`. The Commons-005 fix added
the `Malformed` status precisely so a caller can tell a recoverable legacy row from
unparseable data; this path re-introduces a softer version of the same confusion — a
genuinely broken current-shape row is presented to the user as a benign "please re-save"
legacy row, and the offending field is silently dropped by `FromFlatDict` (which ignores
keys it cannot parse) rather than surfaced. The XML doc describes the legacy fallback as
being for "pre-refactor rows" only and does not mention this branch.
**Recommendation**
Only fall through to `LoadLegacy` when the typed shape is genuinely *not present* — i.e.
the `endpointUrl` property is absent. When `endpointUrl` *is* present but typed
deserialization throws, classify the outcome as `Malformed` (or a distinct status) so the
caller can surface a real error instead of an empty/partial config. Tighten the XML doc
to describe this branch, and add a regression test for a typed row with an invalid enum
field.
**Resolution**
_Unresolved._