diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs index 34571956..17493d6e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs @@ -590,9 +590,23 @@ public class ManagementActor : ReceiveActor var schemaLibrary = sharedSchemas.ToDictionary(s => s.Name, s => s.SchemaJson, StringComparer.Ordinal); Func resolveSchemaRef = name => schemaLibrary.GetValueOrDefault(name); + // #259: also pass shared scripts so a dangling $ref in a SHARED script's schema is + // caught at design-time (not only on the deploy path). Mirror the deploy-path mapping + // in FlatteningPipeline: SharedScript entity → ResolvedScript (name + schema fields). + var sharedScriptEntities = await repo.GetAllSharedScriptsAsync(); + var resolvedSharedScripts = sharedScriptEntities + .Select(s => new Commons.Types.Flattening.ResolvedScript + { + CanonicalName = s.Name, + Code = s.Code, + ParameterDefinitions = s.ParameterDefinitions, + ReturnDefinition = s.ReturnDefinition + }) + .ToList(); + // Run full validation pipeline (collisions, script compilation, trigger refs, bindings) var validationService = new TemplateEngine.Validation.ValidationService(); - var validationResult = validationService.Validate(flatConfig, resolveSchemaRef: resolveSchemaRef); + var validationResult = validationService.Validate(flatConfig, resolvedSharedScripts, resolveSchemaRef: resolveSchemaRef); // Also detect naming collisions across the inheritance/composition graph var svc = sp.GetRequiredService(); diff --git a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs index 9ab96d9d..16c79746 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/ManagementActorTests.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using NSubstitute.ExceptionExtensions; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; @@ -2378,4 +2380,66 @@ public class ManagementActorTests : TestKit, IDisposable // The off-site template (42) must never be scanned. _templateRepo.DidNotReceive().GetTemplateByIdAsync(42, Arg.Any()); } + + // ======================================================================== + // #259: design-time ValidateTemplateCommand passes sharedScripts so a + // dangling $ref in a SHARED script's schema is caught at design time, not + // only on the deploy path. + // ======================================================================== + + [Fact] + public void ValidateTemplate_DanglingRefInSharedScript_ReturnsInvalidResult() + { + // Arrange + var template = new Template("T1") { Id = 1 }; + + _templateRepo.GetTemplateWithChildrenAsync(1, Arg.Any()) + .Returns(template); + _templateRepo.GetAttributesByTemplateIdAsync(1, Arg.Any()) + .Returns(new List()); + _templateRepo.GetAlarmsByTemplateIdAsync(1, Arg.Any()) + .Returns(new List()); + _templateRepo.GetScriptsByTemplateIdAsync(1, Arg.Any()) + .Returns(new List()); + + // Shared script whose parameter schema contains a dangling $ref. + _templateRepo.GetAllSharedScriptsAsync(Arg.Any()) + .Returns(new List + { + new SharedScript("ComputeOrder", "var x = 1;") + { + ParameterDefinitions = """{"$ref":"lib:MissingSharedSchema"}""" + } + }); + + // Schema library is empty → the $ref cannot be resolved and dangles. + var schemaRepo = Substitute.For(); + schemaRepo.ListAsync(Arg.Any()) + .Returns(new List()); + _services.AddScoped(_ => schemaRepo); + + // TemplateService is required by the collision-detection step inside HandleValidateTemplate. + _templateRepo.GetTemplateByIdAsync(1, Arg.Any()) + .Returns(template); + _templateRepo.GetAllTemplatesAsync(Arg.Any()) + .Returns(new List