fix(m9/T26a): HiLo per-setpoint merge in resolver (preview=deploy) + widen staleness comparison
This commit is contained in:
@@ -290,4 +290,213 @@ public class TemplateInheritanceResolverTests
|
||||
}
|
||||
Assert.Equal(flat.Value.Attributes.Count, resolved.Attributes.Count);
|
||||
}
|
||||
|
||||
// ── I1: HiLo per-setpoint merge — the resolved alarm's effective
|
||||
// TriggerConfiguration MUST equal the FlatteningService-merged result for
|
||||
// a derived template that overrides only one setpoint (preview = deploy). ──
|
||||
|
||||
[Fact]
|
||||
public void Resolve_AgreesWithFlatteningService_ForPartialHiLoOverride()
|
||||
{
|
||||
// Base HiLo alarm carries both hi + lo. Derived overrides ONLY hi and
|
||||
// inherits lo. The resolver must surface the MERGED effective trigger
|
||||
// config (lo inherited, hi overridden) — identical to what deploy
|
||||
// produces via the flattener's MergeHiLoConfig.
|
||||
var baseT = new Template("Base") { Id = 1 };
|
||||
baseT.Alarms.Add(new TemplateAlarm("Level")
|
||||
{
|
||||
Id = 10,
|
||||
TemplateId = 1,
|
||||
PriorityLevel = 5,
|
||||
TriggerType = AlarmTriggerType.HiLo,
|
||||
TriggerConfiguration = "{\"hi\":90,\"lo\":10}"
|
||||
});
|
||||
|
||||
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
|
||||
// Partial override: only hi changes; lo must be inherited from base.
|
||||
derived.Alarms.Add(new TemplateAlarm("Level")
|
||||
{
|
||||
Id = 20,
|
||||
TemplateId = 2,
|
||||
PriorityLevel = 5,
|
||||
TriggerType = AlarmTriggerType.HiLo,
|
||||
TriggerConfiguration = "{\"hi\":95}",
|
||||
IsInherited = false
|
||||
});
|
||||
|
||||
var all = new[] { baseT, derived };
|
||||
|
||||
// Resolver view.
|
||||
var resolved = TemplateInheritanceResolver.Resolve(2, all);
|
||||
var resolvedAlarm = Assert.Single(resolved.Alarms, m => m.Name == "Level");
|
||||
|
||||
// Flattener view: most-derived-first chain, no overrides, no compositions.
|
||||
var instance = new Instance("inst") { Id = 1, TemplateId = 2, SiteId = 1 };
|
||||
var chain = new List<Template> { derived, baseT }; // most-derived first
|
||||
var flat = new FlatteningService().Flatten(
|
||||
instance,
|
||||
chain,
|
||||
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
|
||||
new Dictionary<int, IReadOnlyList<Template>>(),
|
||||
new Dictionary<int, DataConnection>());
|
||||
|
||||
Assert.True(flat.IsSuccess, flat.IsFailure ? flat.Error : null);
|
||||
var flatAlarm = Assert.Single(flat.Value.Alarms, a => a.CanonicalName == "Level");
|
||||
|
||||
// The resolver's effective TriggerConfiguration MUST equal the flattener's
|
||||
// merged output verbatim — the key regression guard for preview = deploy.
|
||||
Assert.Equal(flatAlarm.TriggerConfiguration, resolvedAlarm.EffectiveTriggerConfiguration);
|
||||
|
||||
// And sanity: the merge actually happened (lo inherited, hi overridden).
|
||||
Assert.Equal(
|
||||
FlatteningService.MergeHiLoConfig("{\"hi\":90,\"lo\":10}", "{\"hi\":95}"),
|
||||
resolvedAlarm.EffectiveTriggerConfiguration);
|
||||
Assert.Contains("\"lo\":10", resolvedAlarm.EffectiveTriggerConfiguration);
|
||||
Assert.Contains("\"hi\":95", resolvedAlarm.EffectiveTriggerConfiguration);
|
||||
}
|
||||
|
||||
// ── I2: staleness must flag drift on the fields that actually matter, not
|
||||
// just one scalar per type. ──
|
||||
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_TrueWhenBaseAlarmTriggerConfigChanged_PriorityUnchanged()
|
||||
{
|
||||
// Base alarm threshold changed (TriggerConfiguration), PriorityLevel is
|
||||
// unchanged. The derived placeholder still carries the OLD threshold, so
|
||||
// staleness must flag it even though the priority scalar matches.
|
||||
var baseT = new Template("Base") { Id = 1 };
|
||||
baseT.Alarms.Add(new TemplateAlarm("Level")
|
||||
{
|
||||
Id = 10,
|
||||
TemplateId = 1,
|
||||
PriorityLevel = 5,
|
||||
TriggerType = AlarmTriggerType.HiLo,
|
||||
TriggerConfiguration = "{\"hi\":95,\"lo\":10}"
|
||||
});
|
||||
|
||||
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
|
||||
derived.Alarms.Add(new TemplateAlarm("Level")
|
||||
{
|
||||
Id = 20,
|
||||
TemplateId = 2,
|
||||
PriorityLevel = 5, // priority matches the base — only the threshold drifted
|
||||
TriggerType = AlarmTriggerType.HiLo,
|
||||
TriggerConfiguration = "{\"hi\":90,\"lo\":10}", // stale threshold
|
||||
IsInherited = true
|
||||
});
|
||||
|
||||
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
|
||||
|
||||
Assert.True(resolved.Staleness.IsStale);
|
||||
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_TrueWhenNativeSourceConnectionNameChanged_SourceReferenceUnchanged()
|
||||
{
|
||||
var baseT = new Template("Base") { Id = 1 };
|
||||
baseT.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
|
||||
{
|
||||
Id = 10,
|
||||
TemplateId = 1,
|
||||
ConnectionName = "newConn",
|
||||
SourceReference = "ns=2;s=Alarm",
|
||||
ConditionFilter = null
|
||||
});
|
||||
|
||||
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
|
||||
derived.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
|
||||
{
|
||||
Id = 20,
|
||||
TemplateId = 2,
|
||||
ConnectionName = "oldConn", // ConnectionName drifted
|
||||
SourceReference = "ns=2;s=Alarm", // SourceReference unchanged
|
||||
ConditionFilter = null,
|
||||
IsInherited = true
|
||||
});
|
||||
|
||||
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
|
||||
|
||||
Assert.True(resolved.Staleness.IsStale);
|
||||
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_TrueWhenNativeSourceConditionFilterChanged_SourceReferenceUnchanged()
|
||||
{
|
||||
var baseT = new Template("Base") { Id = 1 };
|
||||
baseT.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
|
||||
{
|
||||
Id = 10,
|
||||
TemplateId = 1,
|
||||
ConnectionName = "conn",
|
||||
SourceReference = "ns=2;s=Alarm",
|
||||
ConditionFilter = "severity>500"
|
||||
});
|
||||
|
||||
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
|
||||
derived.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
|
||||
{
|
||||
Id = 20,
|
||||
TemplateId = 2,
|
||||
ConnectionName = "conn",
|
||||
SourceReference = "ns=2;s=Alarm", // unchanged
|
||||
ConditionFilter = null, // ConditionFilter drifted
|
||||
IsInherited = true
|
||||
});
|
||||
|
||||
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
|
||||
|
||||
Assert.True(resolved.Staleness.IsStale);
|
||||
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_StalenessSummary_FalseWhenAlarmAndNativeSourceInSync()
|
||||
{
|
||||
// Guard against false-positives from the widened comparison: when the
|
||||
// stored placeholders match the resolved (merged) values, nothing is stale.
|
||||
var baseT = new Template("Base") { Id = 1 };
|
||||
baseT.Alarms.Add(new TemplateAlarm("Level")
|
||||
{
|
||||
Id = 10,
|
||||
TemplateId = 1,
|
||||
PriorityLevel = 5,
|
||||
TriggerType = AlarmTriggerType.HiLo,
|
||||
TriggerConfiguration = "{\"hi\":90,\"lo\":10}"
|
||||
});
|
||||
baseT.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
|
||||
{
|
||||
Id = 11,
|
||||
TemplateId = 1,
|
||||
ConnectionName = "conn",
|
||||
SourceReference = "ns=2;s=Alarm",
|
||||
ConditionFilter = "severity>500"
|
||||
});
|
||||
|
||||
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
|
||||
derived.Alarms.Add(new TemplateAlarm("Level")
|
||||
{
|
||||
Id = 20,
|
||||
TemplateId = 2,
|
||||
PriorityLevel = 5,
|
||||
TriggerType = AlarmTriggerType.HiLo,
|
||||
TriggerConfiguration = "{\"hi\":90,\"lo\":10}", // in sync
|
||||
IsInherited = true
|
||||
});
|
||||
derived.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
|
||||
{
|
||||
Id = 21,
|
||||
TemplateId = 2,
|
||||
ConnectionName = "conn",
|
||||
SourceReference = "ns=2;s=Alarm",
|
||||
ConditionFilter = "severity>500", // in sync
|
||||
IsInherited = true
|
||||
});
|
||||
|
||||
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
|
||||
|
||||
Assert.False(resolved.Staleness.IsStale);
|
||||
Assert.Equal(0, resolved.Staleness.DifferingMemberCount);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user