feat(commons/transport): ScriptArtifactsChanged invalidation contract + post-commit publish from bundle import (consumer lands in plan 06)
New Commons seam: ScriptArtifactsChanged record + ScriptArtifactKinds + the IScriptArtifactChangeBus pub/sub interface. Host ships InProcessScriptArtifactChangeBus (lock-free copy-on-write, swallow-and-log per subscriber) registered as a central-role singleton. BundleImporter gains an optional IScriptArtifactChangeBus? and, AFTER tx.CommitAsync, publishes one notification per script-bearing kind (ApiMethod/ SharedScript/Template) whose resolution was Overwrite/Rename, using post-resolution names — fully guarded so a bad subscriber can't fail a committed import. Contract + plan-06 handoff table documented. No consumer yet (additive; publisher is correct). Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Scripting;
|
||||
using ZB.MOM.WW.ScadaBridge.Host.Services;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #05-T14: unit tests for the in-process script-artifact change bus — delivery,
|
||||
/// unsubscribe, and the swallow-and-log guard that keeps a faulting subscriber from
|
||||
/// faulting the producer.
|
||||
/// </summary>
|
||||
public sealed class InProcessScriptArtifactChangeBusTests
|
||||
{
|
||||
private static InProcessScriptArtifactChangeBus NewBus() =>
|
||||
new(NullLogger<InProcessScriptArtifactChangeBus>.Instance);
|
||||
|
||||
private static ScriptArtifactsChanged Sample(string kind = ScriptArtifactKinds.ApiMethod) =>
|
||||
new(kind, new[] { "M1" }, "Management", DateTimeOffset.UnixEpoch);
|
||||
|
||||
[Fact]
|
||||
public void Publish_delivers_to_all_current_subscribers()
|
||||
{
|
||||
var bus = NewBus();
|
||||
var a = new List<ScriptArtifactsChanged>();
|
||||
var b = new List<ScriptArtifactsChanged>();
|
||||
bus.Subscribe(a.Add);
|
||||
bus.Subscribe(b.Add);
|
||||
|
||||
var n = Sample();
|
||||
bus.Publish(n);
|
||||
|
||||
Assert.Same(n, Assert.Single(a));
|
||||
Assert.Same(n, Assert.Single(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disposing_subscription_stops_delivery()
|
||||
{
|
||||
var bus = NewBus();
|
||||
var received = new List<ScriptArtifactsChanged>();
|
||||
var token = bus.Subscribe(received.Add);
|
||||
|
||||
bus.Publish(Sample());
|
||||
token.Dispose();
|
||||
bus.Publish(Sample());
|
||||
|
||||
Assert.Single(received); // only the pre-dispose publish landed.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_is_idempotent()
|
||||
{
|
||||
var bus = NewBus();
|
||||
var received = new List<ScriptArtifactsChanged>();
|
||||
var token = bus.Subscribe(received.Add);
|
||||
|
||||
token.Dispose();
|
||||
token.Dispose(); // must not throw nor corrupt the handler list.
|
||||
|
||||
bus.Publish(Sample());
|
||||
Assert.Empty(received);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_faulting_subscriber_does_not_prevent_delivery_to_others_nor_throw()
|
||||
{
|
||||
var bus = NewBus();
|
||||
var good = new List<ScriptArtifactsChanged>();
|
||||
bus.Subscribe(_ => throw new InvalidOperationException("bad consumer"));
|
||||
bus.Subscribe(good.Add);
|
||||
|
||||
// Publish must not propagate the subscriber fault...
|
||||
var ex = Record.Exception(() => bus.Publish(Sample()));
|
||||
Assert.Null(ex);
|
||||
// ...and the well-behaved subscriber still receives it.
|
||||
Assert.Single(good);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Publish_with_no_subscribers_is_a_noop()
|
||||
{
|
||||
var bus = NewBus();
|
||||
Assert.Null(Record.Exception(() => bus.Publish(Sample())));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user