diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs
index ddd9e89c..6587c10a 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs
@@ -1571,6 +1571,8 @@ public sealed class BundleImporter : IBundleImporter
ParameterDefinitions = s.ParameterDefinitions,
ReturnDefinition = s.ReturnDefinition,
IsLocked = s.IsLocked,
+ MinTimeBetweenRuns = s.MinTimeBetweenRuns,
+ ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds,
});
}
return t;
@@ -1854,7 +1856,9 @@ public sealed class BundleImporter : IBundleImporter
!string.Equals(current.TriggerConfiguration, scriptDto.TriggerConfiguration, StringComparison.Ordinal) ||
!string.Equals(current.ParameterDefinitions, scriptDto.ParameterDefinitions, StringComparison.Ordinal) ||
!string.Equals(current.ReturnDefinition, scriptDto.ReturnDefinition, StringComparison.Ordinal) ||
- current.IsLocked != scriptDto.IsLocked;
+ current.IsLocked != scriptDto.IsLocked ||
+ current.MinTimeBetweenRuns != scriptDto.MinTimeBetweenRuns ||
+ current.ExecutionTimeoutSeconds != scriptDto.ExecutionTimeoutSeconds;
if (!changed) continue;
current.Code = scriptDto.Code;
@@ -1863,6 +1867,8 @@ public sealed class BundleImporter : IBundleImporter
current.ParameterDefinitions = scriptDto.ParameterDefinitions;
current.ReturnDefinition = scriptDto.ReturnDefinition;
current.IsLocked = scriptDto.IsLocked;
+ current.MinTimeBetweenRuns = scriptDto.MinTimeBetweenRuns;
+ current.ExecutionTimeoutSeconds = scriptDto.ExecutionTimeoutSeconds;
await _templateRepo.UpdateTemplateScriptAsync(current, ct).ConfigureAwait(false);
await _auditService.LogAsync(
user,
@@ -1889,6 +1895,8 @@ public sealed class BundleImporter : IBundleImporter
ParameterDefinitions = scriptDto.ParameterDefinitions,
ReturnDefinition = scriptDto.ReturnDefinition,
IsLocked = scriptDto.IsLocked,
+ MinTimeBetweenRuns = scriptDto.MinTimeBetweenRuns,
+ ExecutionTimeoutSeconds = scriptDto.ExecutionTimeoutSeconds,
};
ex.Scripts.Add(newScript);
await _auditService.LogAsync(
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/TemplateScriptFidelityTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/TemplateScriptFidelityTests.cs
new file mode 100644
index 00000000..0bbc4ab3
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/TemplateScriptFidelityTests.cs
@@ -0,0 +1,200 @@
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
+using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
+using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
+using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
+using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
+using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
+using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;
+using ZB.MOM.WW.ScadaBridge.DeploymentManager;
+using ZB.MOM.WW.ScadaBridge.TemplateEngine;
+using ZB.MOM.WW.ScadaBridge.Transport;
+using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
+
+namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import;
+
+///
+/// #05-T3: a template script's and
+/// must survive an export→import
+/// round-trip. The exporter serializes both fields (EntitySerializer) and the DTO
+/// carries them; before this fix the importer's BuildTemplate (Add) and
+/// SyncTemplateScriptsAsync (Overwrite) rebuild dropped them, silently resetting
+/// the persisted script to schema defaults. Same in-memory host pattern as
+/// .
+///
+public sealed class TemplateScriptFidelityTests : IDisposable
+{
+ private readonly ServiceProvider _provider;
+
+ public TemplateScriptFidelityTests()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton(
+ new ConfigurationBuilder().AddInMemoryCollection().Build());
+
+ var dbName = $"TemplateScriptFidelityTests_{Guid.NewGuid()}";
+ services.AddDbContext(opts => opts
+ .UseInMemoryDatabase(dbName)
+ .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)));
+
+ services.AddSingleton(new EphemeralDataProtectionProvider());
+ services.AddScoped(sp => new ScadaBridgeDbContext(
+ sp.GetRequiredService>(),
+ sp.GetRequiredService()));
+
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddTransport();
+ services.AddTemplateEngine();
+ services.AddDeploymentManager();
+
+ _provider = services.BuildServiceProvider();
+ }
+
+ public void Dispose() => _provider.Dispose();
+
+ private async Task ExportAndLoadAsync()
+ {
+ Stream bundleStream;
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var exporter = scope.ServiceProvider.GetRequiredService();
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ var templateIds = await ctx.Templates.Select(t => t.Id).ToListAsync();
+ var selection = new ExportSelection(
+ TemplateIds: templateIds,
+ SharedScriptIds: Array.Empty(),
+ ExternalSystemIds: Array.Empty(),
+ DatabaseConnectionIds: Array.Empty(),
+ NotificationListIds: Array.Empty(),
+ SmtpConfigurationIds: Array.Empty(),
+ ApiMethodIds: Array.Empty(),
+ IncludeDependencies: false);
+ bundleStream = await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev",
+ passphrase: null, cancellationToken: CancellationToken.None);
+ }
+
+ using var ms = new MemoryStream();
+ await bundleStream.CopyToAsync(ms);
+ ms.Position = 0;
+
+ await using var loadScope = _provider.CreateAsyncScope();
+ var importer = loadScope.ServiceProvider.GetRequiredService();
+ var session = await importer.LoadAsync(ms, passphrase: null);
+ return session.SessionId;
+ }
+
+ [Fact]
+ public async Task Add_preserves_MinTimeBetweenRuns_and_ExecutionTimeoutSeconds()
+ {
+ // Arrange: seed a template whose script carries non-default cadence + timeout.
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ var t = new Template("Pump") { Description = "fresh" };
+ t.Scripts.Add(new TemplateScript("cadence", "return 1;")
+ {
+ MinTimeBetweenRuns = TimeSpan.FromSeconds(30),
+ ExecutionTimeoutSeconds = 42,
+ });
+ ctx.Templates.Add(t);
+ await ctx.SaveChangesAsync();
+ }
+ var sessionId = await ExportAndLoadAsync();
+
+ // Wipe so the import exercises the Add path.
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ ctx.Templates.RemoveRange(ctx.Templates);
+ await ctx.SaveChangesAsync();
+ }
+
+ // Act
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var importer = scope.ServiceProvider.GetRequiredService();
+ await importer.ApplyAsync(sessionId,
+ new List { new("Template", "Pump", ResolutionAction.Add, null) },
+ user: "bob");
+ }
+
+ // Assert: both fields survived the rebuild.
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ var script = await ctx.Templates
+ .Where(t => t.Name == "Pump")
+ .SelectMany(t => t.Scripts)
+ .SingleAsync(s => s.Name == "cadence");
+ Assert.Equal(TimeSpan.FromSeconds(30), script.MinTimeBetweenRuns);
+ Assert.Equal(42, script.ExecutionTimeoutSeconds);
+ }
+ }
+
+ [Fact]
+ public async Task Overwrite_updates_ExecutionTimeoutSeconds_only_diff_and_emits_audit()
+ {
+ // Arrange: seed script with ExecutionTimeoutSeconds=42; export; then mutate the
+ // target script so it differs from the bundle ONLY in ExecutionTimeoutSeconds.
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ var t = new Template("Pump") { Description = "same" };
+ t.Scripts.Add(new TemplateScript("cadence", "return 1;")
+ {
+ MinTimeBetweenRuns = TimeSpan.FromSeconds(30),
+ ExecutionTimeoutSeconds = 42,
+ });
+ ctx.Templates.Add(t);
+ await ctx.SaveChangesAsync();
+ }
+ var sessionId = await ExportAndLoadAsync();
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ var script = await ctx.Templates
+ .Where(t => t.Name == "Pump")
+ .SelectMany(t => t.Scripts)
+ .SingleAsync(s => s.Name == "cadence");
+ script.ExecutionTimeoutSeconds = 10; // now the only field that differs from the bundle
+ await ctx.SaveChangesAsync();
+ }
+
+ // Act
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var importer = scope.ServiceProvider.GetRequiredService();
+ await importer.ApplyAsync(sessionId,
+ new List { new("Template", "Pump", ResolutionAction.Overwrite, null) },
+ user: "bob");
+ }
+
+ // Assert: field restored to the bundle value AND a TemplateScriptUpdated audit row emitted.
+ await using (var scope = _provider.CreateAsyncScope())
+ {
+ var ctx = scope.ServiceProvider.GetRequiredService();
+ var script = await ctx.Templates
+ .Where(t => t.Name == "Pump")
+ .SelectMany(t => t.Scripts)
+ .SingleAsync(s => s.Name == "cadence");
+ Assert.Equal(42, script.ExecutionTimeoutSeconds);
+
+ Assert.True(await ctx.AuditLogEntries.AnyAsync(a =>
+ a.Action == "TemplateScriptUpdated" && a.EntityName == "Pump.cadence"));
+ }
+ }
+}