fix(modbus): enforce region/type compatibility at the tag factory

P6: Coils/DiscreteInputs carry only Bool and register regions never Bool;
the factory (and so the deploy-gate Inspect and the AdminUI parse consult)
refuses a mismatch, and an absent dataType defaults per region (Bool for a
bit region). A HoldingRegisters+Bool tag used to throw out of the coalescing
planner and stall every tag of the subscription; the planner also skips any
incompatible pair defensively. The tag editor keeps the type compatible when
the region changes; the CLI refuses the pair up front. The array-write parity
test records the Coils divergence from DriverArrayWriteSupport (owned outside
this lane) as follow-up F1.

Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 15:19:28 -04:00
parent 620ecc41dd
commit f827b3a134
9 changed files with 191 additions and 4 deletions
@@ -25,10 +25,43 @@ public sealed class ModbusArrayWriteSupportParityTests
return data;
}
/// <summary>
/// Shapes where the driver has moved ahead of the server-side table (review 07 P6 / U3, Modbus lane) and
/// <see cref="DriverArrayWriteSupport"/> — owned outside the Modbus lane — has not followed yet
/// (follow-up F1): the tag factory now REFUSES a Coils tag with a non-Bool type (so the driver serves no
/// such tag), and the driver now encodes String[] and BitInRegister[] holding-register writes.
/// <see cref="Pending_F1_divergences_still_exist"/> fails the moment the table follows, so this list
/// is removed with it rather than left to rot.
/// </summary>
private static bool PendingF1(ModbusRegion region, ModbusDataType dataType) =>
(region == ModbusRegion.Coils && dataType != ModbusDataType.Bool)
|| (region == ModbusRegion.HoldingRegisters && dataType is ModbusDataType.String or ModbusDataType.BitInRegister && PendingU3);
// Flipped to true by the U3 commit (String[] / BitInRegister[] encoders).
private const bool PendingU3 = false;
public static TheoryData<ModbusRegion, ModbusDataType> PendingShapes()
{
var data = new TheoryData<ModbusRegion, ModbusDataType>();
foreach (var (region, dt) in Shapes().Where(s => PendingF1(s.Region, s.DataType))) data.Add(region, dt);
return data;
}
[Theory]
[MemberData(nameof(PendingShapes))]
public void Pending_F1_divergences_still_exist(ModbusRegion region, ModbusDataType dataType)
{
var json = $$"""{"region":"{{region}}","address":10,"dataType":"{{dataType}}","stringLength":4,"isArray":true,"arrayLength":2}""";
var driverAccepts = region == ModbusRegion.HoldingRegisters; // String[] / BitInRegister[] encode; mistyped Coils are refused
DriverArrayWriteSupport.Supports(DriverTypeNames.Modbus, "Int16", json).ShouldNotBe(driverAccepts,
"F1 landed — DriverArrayWriteSupport now agrees with the driver for this shape; delete it from PendingF1");
}
[Theory]
[MemberData(nameof(AllShapes))]
public async Task Table_agrees_with_the_driver_write_path(ModbusRegion region, ModbusDataType dataType)
{
if (PendingF1(region, dataType)) return; // covered by Pending_F1_divergences_still_exist
var json = $$"""{"region":"{{region}}","address":10,"dataType":"{{dataType}}","stringLength":4,"isArray":true,"arrayLength":2}""";
var fake = new ModbusDriverTests.FakeTransport();
var drv = new ModbusDriver(