fix(adminui): guard the driver dispatch maps and close G-1..G-6 (§8.4)
A parity guard already existed for DriverTypeNames <-> the /raw driver picker, but not for the two dispatch maps downstream, so a driver could be registered, offered in the picker, and then have no config form. That is exactly what happened to Calculation (G-1) and to Sql/MTConnect/Calculation on the device modal (G-2) — both survived review because nothing could see them. The maps had to become data first. A Razor @switch compiles into BuildRenderTree's IL, so no test can enumerate its cases; the picker guard works only because RawDriverTypeDialog keeps its data in a field the markup enumerates. Both modals now render from DriverConfigFormMap / DeviceFormMap through <DynamicComponent>, and being public those maps need none of the picker test's BindingFlags.NonPublic fragility. - G-1 CalculationDriverForm.razor — RunTimeout was unauthorable via the UI. - G-2 Sql/MTConnect/Calculation declared single-connection rather than given hollow device forms; each holds one connection at the driver level. - G-3 DriverConfigModal's hardcoded "Galaxy or Mqtt" -> IsSingleConnection, so Sql/MTConnect authors are no longer sent to a device form with no endpoint. - G-4 DriverFormMapParityTests (5, both directions) + DriverDispatchMapParityTests. - G-5 CsvColumnMap entries for Sql, Mqtt, MTConnect. - G-6 RawBrowseCommitMapper Sql branch — and the browser end, which the audit missed: SqlBrowseSession emitted NO AddressFields, so a browsed leaf carried only a column name, and a column alone cannot address a Sql tag. It now travels schema/table/column and the mapper builds a WideRow config from them. A branch alone would have produced a half-built blob. Two findings while writing the guards. The G-6 test showed Modbus and Calculation also hit the generic address fallback — correctly, as neither is browsable — so it asserts the fall-through set EQUALS a documented non-browsable list in both directions; a one-way check would let a newly browsable driver be quietly added to the exclusion list. And TagConfigDriverTypeNameGuardTests was hollow: it enumerated a hand-written TheoryData that had already drifted (omitting Galaxy, Sql and Mqtt), guarding renames but not gaps. Now enumerated from the map with a coverage test facing the other way. Live-verified on docker-dev, since this repo has no bUnit and no unit test reaches Blazor parameter binding: opened Calculation's config (previously "No typed config form"), typed 3500, saved, reopened — the value persisted, so the DynamicComponent two-way binding round-trips. Sql's form renders fully and now reads "This driver holds a single connection, authored above". AdminUI.Tests 930 passed.
This commit is contained in:
+46
-3
@@ -355,8 +355,9 @@ appear in §7.1 as well.
|
||||
so it was deleted rather than fixed. See §9.
|
||||
3. ~~**#516** — silent config discard on 5 drivers; then re-scope #489, which it partly subsumes.~~
|
||||
✅ **Done.** See §9. #489 is still open and should now be re-scoped.
|
||||
4. **G-4** — extend the existing picker-parity test to `DriverConfigModal` + `DeviceModal`; it would
|
||||
have caught G-1 and G-2 for free, and this class has now recurred twice.
|
||||
4. ~~**G-4** — extend the existing picker-parity test to `DriverConfigModal` + `DeviceModal`; it would
|
||||
have caught G-1 and G-2 for free, and this class has now recurred twice.~~ ✅ **Done**, together with
|
||||
G-1, G-2, G-3, G-5 and G-6. See §9.
|
||||
5. **Bookkeeping sweep** — mark the 8 stale `.tasks.json` files complete so the 19-task AdminUI plan
|
||||
(§6.2) is visible as the real backlog it is.
|
||||
6. **§3.3 tier truth** — either pass real tiers at factory registration or delete the Tier-C
|
||||
@@ -374,7 +375,7 @@ the work, so this register never disagrees with the tree.
|
||||
| 1. ACL enforcement decision | ✅ **Done** — decided *make non-enforcement explicit*; Gitea **#520** tracks the wire-up | `d1e88dc4` |
|
||||
| 2. #518 + #507 | ✅ **Done** — `IRediscoverable` consumed as a re-browse prompt; #507's injection path **deleted**; `IHostConnectivityProbe` half split to Gitea **#521** | `09a401b8`, `97c9f4b4` |
|
||||
| 3. #516 config discard | ✅ **Done** — seam respawn + re-parse on 3 drivers; Sql/FOCAS deliberately respawn-only | `2dc19f30` |
|
||||
| 4. G-4 dispatch-map parity | ⏳ Not started | — |
|
||||
| 4. G-4 dispatch-map parity | ✅ **Done** — G-1…G-6 all closed, live-verified on docker-dev | `abacf4cf` |
|
||||
| 5. Bookkeeping sweep | ⏳ Not started | — |
|
||||
| 6. Tier truth | ⏳ Not started — decided *document the truth, defer the code decision* | — |
|
||||
|
||||
@@ -465,6 +466,48 @@ Two traps worth keeping:
|
||||
`DriverHostStatus` table was re-created deliberately in the v3 initial migration, so deleting on
|
||||
inference would be wrong. Split to Gitea **#521** with both options costed.
|
||||
|
||||
### 2026-07-27 — §8.4 dispatch-map parity (G-1 … G-6)
|
||||
|
||||
**The maps had to become data before they could be guarded.** A Razor `@switch` compiles into
|
||||
`BuildRenderTree`'s IL, so nothing can enumerate its cases — that is the whole reason G-1 and G-2
|
||||
survived review while the sibling picker guard stayed green (the picker test works only because
|
||||
`RawDriverTypeDialog` keeps its data in a *field* the markup enumerates). Both modals now render from
|
||||
`DriverConfigFormMap` / `DeviceFormMap` via `<DynamicComponent>`, and being `public` those maps need
|
||||
none of the picker test's `BindingFlags.NonPublic` fragility.
|
||||
|
||||
| Gap | Resolution |
|
||||
|---|---|
|
||||
| G-1 | `CalculationDriverForm.razor` added — `RunTimeout` was unauthorable through the UI. |
|
||||
| G-2 | Sql / MTConnect / Calculation are declared **single-connection** rather than given hollow device forms — each holds one connection at the driver level, so a per-device endpoint editor would be meaningless. |
|
||||
| G-3 | `DriverConfigModal`'s hardcoded `Galaxy or Mqtt` replaced by `DeviceFormMap.IsSingleConnection`. |
|
||||
| G-4 | `DriverFormMapParityTests` (5 cases, both directions) + `DriverDispatchMapParityTests` (3). |
|
||||
| G-5 | `CsvColumnMap` entries for Sql, Mqtt, MTConnect. |
|
||||
| G-6 | `RawBrowseCommitMapper` Sql branch — **and the browser end too**, which the register missed. |
|
||||
|
||||
**G-6 was bigger than filed.** The register said the mapper lacked a Sql branch. It also turned out that
|
||||
`SqlBrowseSession` emitted **no `AddressFields` at all**, so a browsed leaf carried only a column name —
|
||||
and a column name alone cannot address a Sql tag, which needs its table. The browser now travels
|
||||
schema/table/column and the mapper builds a `WideRow` `SqlTagConfigModel` from them. A branch alone
|
||||
would have produced a half-built blob.
|
||||
|
||||
**Two extra findings while writing the guards:**
|
||||
|
||||
- The G-6 test found **Modbus and Calculation** also fall through to the generic `{"address": …}` key.
|
||||
Both are correct — neither is browsable — so the test asserts the fall-through set **equals** a
|
||||
documented non-browsable list, in both directions. A one-way check would let a newly-browsable driver
|
||||
be quietly added to the exclusion list instead of getting a branch.
|
||||
- `TagConfigDriverTypeNameGuardTests` was **hollow**: it enumerated a hand-written `TheoryData` that had
|
||||
already drifted (omitting Galaxy, Sql and Mqtt), so it guarded against renames but not gaps — a new
|
||||
`DriverTypeNames` constant would simply never appear. Now enumerated from the map, paired with a
|
||||
coverage test in the other direction.
|
||||
|
||||
**Live-verified on docker-dev** (`localhost:9200`, central pair rebuilt), because this repo has no bUnit
|
||||
and no unit test can cover Blazor parameter binding: opened Calculation's config — the form renders where
|
||||
it previously showed "No typed config form" — typed `3500`, saved, reopened, and the value **persisted**,
|
||||
proving the `DynamicComponent` two-way binding round-trips. Sql's form renders fully (regression check on
|
||||
the conversion) and now reads "This driver holds a single connection, authored above" (G-3). The rebuilt
|
||||
image also showed `RediscoveryNeededUtc`/`RediscoveryReason` on the wire, confirming §8.2 end-to-end.
|
||||
|
||||
### 2026-07-27 — §8.3 #516 driver config edits silently discarded
|
||||
|
||||
Decision: **both** — per-driver re-parse *and* the seam respawn. Reading the drivers split the
|
||||
|
||||
Reference in New Issue
Block a user