fix(correctness): close Theme 10 — 5 data-integrity / serialisation findings
Final themed batch. 5 well-localised correctness fixes. Serialisation precision: - ESG-020: DatabaseGateway.JsonElementToParameterValue probes TryGetInt64 → TryGetDecimal → GetDouble, so a script's high-precision decimal SQL parameter survives the cached-write retry round-trip without silent precision loss. 3 new regression tests. Template engine correctness: - TE-018: DiffService gains ComputeConnectionsDiff over FlattenedConfiguration.Connections, mirroring the existing entity-diff shape and pairing with the Theme 1 TE-017 hash-coverage fix. A ConfigurationDiff record extension in Commons is flagged as a follow-up. - TE-019: TemplateResolver.BuildInheritanceChain now walks via the int? ParentTemplateId directly — only null means "no parent". A real Id of 0 (the prior special-cased sentinel) now walks the chain like any other node, matching the TemplateEngine-013 CycleDetector fix. Regression of TE-013 closed. - TE-020: All 5 Create* paths in TemplateService + SharedScriptService re-ordered to save-first → log-with-real-Id → save-audit (matching the InstanceService pattern). Create* audit rows no longer carry a literal "0" EntityId. Doc deferral: - Transport-012: Component-Transport.md §Audit Trail now spells out that the BundleImportId repository filter IS wired (in CentralUiRepository), but the Audit-Log-Viewer UI dropdown + summary-row hyperlink are a deferred CentralUI follow-up. CLI workaround documented (audit query --bundle-import-id). 11+ new regression tests (3 ESG, 4 DiffService, 3 TemplateResolver, 4 TemplateService, 1 SharedScriptService). Build clean; ESG 72/72, TemplateEngine 324/324. README regenerated: 1 pending of 481 total. Session-to-date: 135 of 136 originally-open Theme findings closed across 10 themes in 10 commits.
This commit is contained in:
@@ -251,4 +251,122 @@ public class DiffServiceTests
|
||||
|
||||
Assert.False(DiffService.ConnectionsEqual(a, b));
|
||||
}
|
||||
|
||||
// ── TemplateEngine-018: ComputeConnectionsDiff produces Added/Removed/Changed entries ──
|
||||
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_NewBindingAdded_ReportedAsAdded()
|
||||
{
|
||||
// First-time binding (or instance gains its first data-sourced
|
||||
// attribute) — old config has no Connections map, new config does.
|
||||
// The pre-018 diff shape silently dropped this so operators saw
|
||||
// "no changes" when the deployment package was structurally larger.
|
||||
var oldConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1" };
|
||||
var newConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host\"}",
|
||||
FailoverRetryCount = 3,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var diff = _sut.ComputeConnectionsDiff(oldConfig, newConfig);
|
||||
|
||||
Assert.Single(diff);
|
||||
Assert.Equal("plc1", diff[0].CanonicalName);
|
||||
Assert.Equal(DiffChangeType.Added, diff[0].ChangeType);
|
||||
Assert.Null(diff[0].OldValue);
|
||||
Assert.NotNull(diff[0].NewValue);
|
||||
Assert.Equal("OpcUa", diff[0].NewValue!.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_BindingCleared_ReportedAsRemoved()
|
||||
{
|
||||
// Last data-sourced attribute removed — old config carried a
|
||||
// connection, new config does not.
|
||||
var oldConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig { Protocol = "OpcUa", ConfigurationJson = "{}" }
|
||||
}
|
||||
};
|
||||
var newConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1" };
|
||||
|
||||
var diff = _sut.ComputeConnectionsDiff(oldConfig, newConfig);
|
||||
|
||||
Assert.Single(diff);
|
||||
Assert.Equal("plc1", diff[0].CanonicalName);
|
||||
Assert.Equal(DiffChangeType.Removed, diff[0].ChangeType);
|
||||
Assert.NotNull(diff[0].OldValue);
|
||||
Assert.Null(diff[0].NewValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_EndpointEdit_ReportedAsChanged()
|
||||
{
|
||||
// A connection-endpoint edit must surface as a Changed diff entry —
|
||||
// the deployment package will ship a different ConnectionConfig and
|
||||
// the operator-facing diff view must say so.
|
||||
var oldConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host-a:4840\"}",
|
||||
FailoverRetryCount = 3,
|
||||
}
|
||||
}
|
||||
};
|
||||
var newConfig = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig
|
||||
{
|
||||
Protocol = "OpcUa",
|
||||
ConfigurationJson = "{\"endpoint\":\"opc.tcp://host-b:4840\"}",
|
||||
FailoverRetryCount = 3,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var diff = _sut.ComputeConnectionsDiff(oldConfig, newConfig);
|
||||
|
||||
Assert.Single(diff);
|
||||
Assert.Equal("plc1", diff[0].CanonicalName);
|
||||
Assert.Equal(DiffChangeType.Changed, diff[0].ChangeType);
|
||||
Assert.Contains("host-a", diff[0].OldValue!.ConfigurationJson);
|
||||
Assert.Contains("host-b", diff[0].NewValue!.ConfigurationJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeConnectionsDiff_IdenticalConnections_NoEntries()
|
||||
{
|
||||
// Sanity check: an unchanged connection produces no diff entry, so
|
||||
// ComputeConnectionsDiff stays quiet when nothing relevant has
|
||||
// changed.
|
||||
var connections = new Dictionary<string, ConnectionConfig>
|
||||
{
|
||||
["plc1"] = new ConnectionConfig { Protocol = "OpcUa", ConfigurationJson = "{}" }
|
||||
};
|
||||
var oldConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Connections = connections };
|
||||
var newConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1", Connections = connections };
|
||||
|
||||
var diff = _sut.ComputeConnectionsDiff(oldConfig, newConfig);
|
||||
|
||||
Assert.Empty(diff);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user