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:
@@ -174,4 +174,80 @@ public class TemplateResolverTests
|
||||
Assert.Equal("P", chain[1].Name);
|
||||
Assert.Equal("C", chain[2].Name);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-019: a real Id of 0 must walk the inheritance chain ──
|
||||
|
||||
[Fact]
|
||||
public void BuildInheritanceChain_RealIdZero_IsTreatedAsParentReferenceNotAsNoParent()
|
||||
{
|
||||
// TemplateEngine-013 removed the 0-as-no-parent sentinel from
|
||||
// CycleDetector; the same fix had not propagated into the resolver,
|
||||
// so seeding BuildInheritanceChain with templateId == 0 returned an
|
||||
// empty chain even when a template with Id 0 existed (e.g. an
|
||||
// import-staging row, or any in-memory test setup). Post-019: only a
|
||||
// null ParentTemplateId means "no parent", and an Id of 0 walks the
|
||||
// chain like any other node.
|
||||
var orphaned = new Template("OrphanedId0") { Id = 0 };
|
||||
orphaned.Attributes.Add(new TemplateAttribute("Speed")
|
||||
{
|
||||
Id = 1,
|
||||
TemplateId = 0,
|
||||
DataType = DataType.Float,
|
||||
});
|
||||
|
||||
var lookup = new Dictionary<int, Template> { [0] = orphaned };
|
||||
var chain = TemplateResolver.BuildInheritanceChain(0, lookup);
|
||||
|
||||
// Pre-019: while (currentId != 0 && ...) was false on the first
|
||||
// iteration, so the chain was empty and the orphaned template's
|
||||
// members were silently dropped from every flatten/resolve through
|
||||
// it. Post-019: the orphan is the chain.
|
||||
Assert.Single(chain);
|
||||
Assert.Equal("OrphanedId0", chain[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildInheritanceChain_ParentChainThroughIdZero_DoesNotTruncateChainAtZero()
|
||||
{
|
||||
// A template whose real parent has Id 0 must include the Id 0 parent
|
||||
// in the chain. Pre-019: `current.ParentTemplateId ?? 0` paired with
|
||||
// `currentId != 0` exited the loop as soon as the walk reached a real
|
||||
// Id of 0 — silently truncating the inheritance contribution from the
|
||||
// root template.
|
||||
var parent = new Template("ParentWithIdZero") { Id = 0 };
|
||||
parent.Attributes.Add(new TemplateAttribute("RootAttr")
|
||||
{
|
||||
Id = 1,
|
||||
TemplateId = 0,
|
||||
DataType = DataType.String,
|
||||
});
|
||||
var child = new Template("Child") { Id = 5, ParentTemplateId = 0 };
|
||||
|
||||
var lookup = new Dictionary<int, Template> { [0] = parent, [5] = child };
|
||||
var chain = TemplateResolver.BuildInheritanceChain(5, lookup);
|
||||
|
||||
Assert.Equal(2, chain.Count);
|
||||
Assert.Equal("ParentWithIdZero", chain[0].Name); // root first
|
||||
Assert.Equal("Child", chain[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveAllMembers_TemplateWithRealIdZero_StillResolvesItsMembers()
|
||||
{
|
||||
// End-to-end: ResolveAllMembers piggybacks on BuildInheritanceChain,
|
||||
// so the chain truncation regression also dropped every member of an
|
||||
// Id-0 template from the resolved-member set. Lock this in too.
|
||||
var orphan = new Template("OrphanedId0") { Id = 0 };
|
||||
orphan.Attributes.Add(new TemplateAttribute("Speed")
|
||||
{
|
||||
Id = 1,
|
||||
TemplateId = 0,
|
||||
DataType = DataType.Float,
|
||||
});
|
||||
|
||||
var members = TemplateResolver.ResolveAllMembers(0, new List<Template> { orphan });
|
||||
|
||||
Assert.Single(members);
|
||||
Assert.Equal("Speed", members[0].CanonicalName);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user