fix(template-engine): native alarm sources participate in the revision hash — staleness detection restored (deliberate one-time stale flip for affected instances)
TemplateEngine-011: ResolvedNativeAlarmSource bindings were absent from the flattened-config revision hash, so edits to a native alarm source binding (source reference, connection, condition filter, locked state) went undetected by staleness detection. Add a HashableNativeAlarmSource record and fold the sorted, null-when-empty NativeAlarmSources collection into HashableConfiguration in its alphabetical slot. WhenWritingNull keeps every native-source-free config byte-identical to its pre-migration hash (pinned literal test proves it), so the migration is surgical: only native-source-bearing instances flip stale once after upgrade; redeploy clears it — deliberate. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
+97
@@ -367,6 +367,103 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_NativeAlarmSourceChange_ChangesHash()
|
||||
{
|
||||
// TemplateEngine-011: native alarm source bindings are part of the
|
||||
// deployment package, so an edit to a source's binding (here the
|
||||
// SourceReference) MUST change the revision hash and flag the instance
|
||||
// stale. Before this migration native sources were absent from the hash
|
||||
// and this edit went undetected.
|
||||
var baseSource = new ResolvedNativeAlarmSource
|
||||
{
|
||||
CanonicalName = "Boiler.Alarms",
|
||||
ConnectionName = "plc1",
|
||||
SourceReference = "ns=2;s=Boiler",
|
||||
ConditionFilter = null,
|
||||
IsLocked = false
|
||||
};
|
||||
var editedSource = baseSource with { SourceReference = "ns=2;s=Boiler2" };
|
||||
|
||||
var configBefore = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
TemplateId = 1,
|
||||
SiteId = 1,
|
||||
NativeAlarmSources = [baseSource]
|
||||
};
|
||||
var configAfter = configBefore with { NativeAlarmSources = [editedSource] };
|
||||
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_NativeAlarmSourceIsLockedChange_ChangesHash()
|
||||
{
|
||||
// TemplateEngine-011: IsLocked participates in the hash too.
|
||||
var baseSource = new ResolvedNativeAlarmSource
|
||||
{
|
||||
CanonicalName = "Boiler.Alarms",
|
||||
ConnectionName = "plc1",
|
||||
SourceReference = "ns=2;s=Boiler",
|
||||
IsLocked = false
|
||||
};
|
||||
var editedSource = baseSource with { IsLocked = true };
|
||||
|
||||
var configBefore = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
TemplateId = 1,
|
||||
SiteId = 1,
|
||||
NativeAlarmSources = [baseSource]
|
||||
};
|
||||
var configAfter = configBefore with { NativeAlarmSources = [editedSource] };
|
||||
|
||||
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_NativeAlarmSourceOrder_DoesNotAffectHash()
|
||||
{
|
||||
// TemplateEngine-011: sources are sorted by CanonicalName before hashing,
|
||||
// mirroring attributes/alarms/scripts — authoring order is irrelevant.
|
||||
var a = new ResolvedNativeAlarmSource
|
||||
{
|
||||
CanonicalName = "A", ConnectionName = "plc1", SourceReference = "ns=2;s=A"
|
||||
};
|
||||
var b = new ResolvedNativeAlarmSource
|
||||
{
|
||||
CanonicalName = "B", ConnectionName = "plc1", SourceReference = "ns=2;s=B"
|
||||
};
|
||||
|
||||
var config1 = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
TemplateId = 1,
|
||||
SiteId = 1,
|
||||
NativeAlarmSources = [a, b]
|
||||
};
|
||||
var config2 = config1 with { NativeAlarmSources = [b, a] };
|
||||
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeHash_NoNativeAlarmSources_HashUnchangedFromBaseline()
|
||||
{
|
||||
// TemplateEngine-011 migration surgicality: adding the (null-when-empty)
|
||||
// NativeAlarmSources property to the hashable projection must NOT change
|
||||
// the hash of any native-source-free configuration. The literal below is
|
||||
// the pre-migration hash of CreateConfig("Instance1", "25.0"); pinning it
|
||||
// proves that only instances that HAVE native sources flip stale — there
|
||||
// is no global staleness storm.
|
||||
var config = CreateConfig("Instance1", "25.0");
|
||||
|
||||
Assert.Equal(
|
||||
"sha256:e937bcfa34a146ca6bdb9bda27bdceb204e440a4a6110ca646f7750f37b166cb",
|
||||
_sut.ComputeHash(config));
|
||||
}
|
||||
|
||||
private static FlattenedConfiguration CreateConfig(string instanceName, string tempValue)
|
||||
{
|
||||
return new FlattenedConfiguration
|
||||
|
||||
Reference in New Issue
Block a user