fix(transport): import reads the template library once per session, batches instance lookups, and expands stale instances to derived/composing templates over one flatten session (review 2026-10-21 04/P4, 08/P1, 04/C2)
This commit is contained in:
+105
@@ -1505,6 +1505,111 @@ public sealed class BundleImporterApplyTests : IDisposable
|
||||
Assert.DoesNotContain(valveInstanceId, result.StaleInstanceIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 04/C2: overwriting a feature MODULE changes the flattened
|
||||
/// config of every template that composes it, so a deployed instance of the
|
||||
/// composing template — which has no direct relationship to the overwritten
|
||||
/// template id — must be reported stale.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ApplyAsync_overwriting_a_composed_module_marks_instances_of_the_composing_template_stale()
|
||||
{
|
||||
await SeedSiteAsync();
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var module = new Template("Motor") { Description = "module" };
|
||||
module.Attributes.Add(new TemplateAttribute("Speed") { DataType = DataType.Float, Value = "1.0" });
|
||||
module.Attributes.Add(new TemplateAttribute("Torque") { DataType = DataType.Float, Value = "2.0" });
|
||||
ctx.Templates.Add(module);
|
||||
await ctx.SaveChangesAsync();
|
||||
var station = new Template("Station") { Description = "composer" };
|
||||
station.Compositions.Add(new TemplateComposition("motor1") { ComposedTemplateId = module.Id });
|
||||
ctx.Templates.Add(station);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
var sessionId = await ExportAndLoadAsync();
|
||||
|
||||
// Drop Torque from the target module, then deploy a Station instance against
|
||||
// that shape; the Overwrite restores Torque inside the composed slot.
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var module = await ctx.Templates.Include(x => x.Attributes).SingleAsync(x => x.Name == "Motor");
|
||||
var torque = module.Attributes.Single(a => a.Name == "Torque");
|
||||
module.Attributes.Remove(torque);
|
||||
ctx.TemplateAttributes.Remove(torque);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
var stationInstanceId = await SeedDeployedInstanceWithRealSnapshotAsync("Station", "Station-Deployed");
|
||||
|
||||
ImportResult result;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
result = await importer.ApplyAsync(sessionId,
|
||||
new List<ImportResolution>
|
||||
{
|
||||
new("Template", "Motor", ResolutionAction.Overwrite, null),
|
||||
new("Template", "Station", ResolutionAction.Skip, null),
|
||||
},
|
||||
user: "bob");
|
||||
}
|
||||
|
||||
Assert.Contains(stationInstanceId, result.StaleInstanceIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 04/C2: overwriting a BASE template changes every derived
|
||||
/// template's flattened config, so a deployed instance of a derived template must
|
||||
/// be reported stale even though no instance uses the base directly.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ApplyAsync_overwriting_a_base_template_marks_instances_of_derived_templates_stale()
|
||||
{
|
||||
await SeedSiteAsync();
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var baseT = new Template("Receiver") { Description = "base" };
|
||||
baseT.Attributes.Add(new TemplateAttribute("Flow") { DataType = DataType.Float, Value = "1.0" });
|
||||
baseT.Attributes.Add(new TemplateAttribute("Pressure") { DataType = DataType.Float, Value = "2.0" });
|
||||
ctx.Templates.Add(baseT);
|
||||
await ctx.SaveChangesAsync();
|
||||
ctx.Templates.Add(new Template("Reactor") { Description = "derived", ParentTemplateId = baseT.Id });
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
var sessionId = await ExportAndLoadAsync();
|
||||
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var baseT = await ctx.Templates.Include(x => x.Attributes).SingleAsync(x => x.Name == "Receiver");
|
||||
var pressure = baseT.Attributes.Single(a => a.Name == "Pressure");
|
||||
baseT.Attributes.Remove(pressure);
|
||||
ctx.TemplateAttributes.Remove(pressure);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
var reactorInstanceId = await SeedDeployedInstanceWithRealSnapshotAsync("Reactor", "Reactor-Deployed");
|
||||
|
||||
ImportResult result;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
result = await importer.ApplyAsync(sessionId,
|
||||
new List<ImportResolution>
|
||||
{
|
||||
new("Template", "Receiver", ResolutionAction.Overwrite, null),
|
||||
new("Template", "Reactor", ResolutionAction.Skip, null),
|
||||
},
|
||||
user: "bob");
|
||||
}
|
||||
|
||||
Assert.Contains(reactorInstanceId, result.StaleInstanceIds);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a freshly imported NotDeployed instance is never reported as stale.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user