From bae75be2d27034ab57427b30574da616202be7e0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 24 May 2026 07:52:31 -0400 Subject: [PATCH] fix(transport): stop scanning DataSourceReference for blocker references DetectBlockersAsync was feeding TemplateAttribute.DataSourceReference into the identifier scanner alongside script bodies, but that field is an OPC UA node-address path (e.g. "ns=3;s=Tank.Level") owned by the device, not script source. The dot delimiter inside the path tripped the heuristic into flagging the address segment ("Tank", "Sensor", "TestChildObject", "DevAppEngine") as a missing SharedScript or ExternalSystem reference -- a 100% false-positive class on any template catalog with OPC-UA-mapped attributes. Drop the DataSourceReference scan entirely. Attribute.Value is still scanned because it can carry a design-time default expression that calls into runtime APIs. Add a regression test pinning the new behavior. --- .../Import/BundleImporter.cs | 7 +++- .../Import/BundleImporterPreviewTests.cs | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/ScadaLink.Transport/Import/BundleImporter.cs b/src/ScadaLink.Transport/Import/BundleImporter.cs index a388f64..f2cbccb 100644 --- a/src/ScadaLink.Transport/Import/BundleImporter.cs +++ b/src/ScadaLink.Transport/Import/BundleImporter.cs @@ -374,8 +374,13 @@ public sealed class BundleImporter : IBundleImporter foreach (var s in t.Scripts) CollectCallIdentifiers(s.Code, referencedFromBundle); foreach (var a in t.Attributes) { + // Attribute.Value carries the design-time default expression, which + // can be script-callable. DataSourceReference is an OPC UA node + // address path (e.g. "ns=3;s=Tank.Level") owned by the device -- + // it's never script source and must NOT be scanned, or the dot + // delimiter trips the heuristic into flagging the address segments + // as missing SharedScript/ExternalSystem references. CollectCallIdentifiers(a.Value, referencedFromBundle); - CollectCallIdentifiers(a.DataSourceReference, referencedFromBundle); } } foreach (var m in content.ApiMethods) diff --git a/tests/ScadaLink.Transport.IntegrationTests/Import/BundleImporterPreviewTests.cs b/tests/ScadaLink.Transport.IntegrationTests/Import/BundleImporterPreviewTests.cs index 4cf2ce9..f4060d3 100644 --- a/tests/ScadaLink.Transport.IntegrationTests/Import/BundleImporterPreviewTests.cs +++ b/tests/ScadaLink.Transport.IntegrationTests/Import/BundleImporterPreviewTests.cs @@ -251,6 +251,45 @@ public sealed class BundleImporterPreviewTests : IDisposable i.Kind == ConflictKind.Blocker && i.Name == "HelperFn"); } + [Fact] + public async Task PreviewAsync_does_not_flag_opcua_tag_paths_in_DataSourceReference_as_blockers() + { + // Arrange: a template with an attribute whose DataSourceReference is an + // OPC UA node-address path -- e.g. "ns=3;s=Tank.Level". The segment + // before the dot ("Tank") used to be parsed by the blocker heuristic as + // a potential SharedScript reference, even though tag paths live in the + // device's address space and are not script-callable. + await using (var scope = _provider.CreateAsyncScope()) + { + var ctx = scope.ServiceProvider.GetRequiredService(); + var t = new Template("Pump") { Description = "tag-path-check" }; + t.Attributes.Add(new TemplateAttribute("Level") + { + Value = "0", + DataSourceReference = "ns=3;s=Tank.Level", + }); + ctx.Templates.Add(t); + await ctx.SaveChangesAsync(); + } + + var bundleStream = await ExportTemplatesAsync(); + var bytes = await StreamToBytes(bundleStream); + + // Act + ImportPreview preview; + await using (var scope = _provider.CreateAsyncScope()) + { + var importer = scope.ServiceProvider.GetRequiredService(); + var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null); + preview = await importer.PreviewAsync(session.SessionId); + } + + // Assert: "Tank" (the device-owned tag-path root segment) must not be + // flagged as a missing SharedScript or ExternalSystem reference. + Assert.DoesNotContain(preview.Items, i => + i.Kind == ConflictKind.Blocker && i.Name == "Tank"); + } + [Fact] public async Task PreviewAsync_does_not_flag_stdlib_or_runtime_member_accesses_as_blockers() {