using Microsoft.Extensions.Options; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests; /// /// values size the staggered Instance-Actor /// startup batches, the dedicated script-execution thread scheduler, and the /// site-wide stream buffer. A zero/negative count crashes actor construction with /// an opaque ; these tests assert the /// rejects a bad /// ScadaBridge:SiteRuntime section with a clear, key-naming message. /// public class SiteRuntimeOptionsValidatorTests { private static ValidateOptionsResult Validate(SiteRuntimeOptions options) => new SiteRuntimeOptionsValidator().Validate(Options.DefaultName, options); [Fact] public void DefaultOptions_AreValid() { var result = Validate(new SiteRuntimeOptions()); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void ZeroStartupBatchSize_IsRejected() { var result = Validate(new SiteRuntimeOptions { StartupBatchSize = 0 }); Assert.True(result.Failed); Assert.Contains("StartupBatchSize", result.FailureMessage); } [Fact] public void ZeroScriptExecutionThreadCount_IsRejected() { var result = Validate(new SiteRuntimeOptions { ScriptExecutionThreadCount = 0 }); Assert.True(result.Failed); Assert.Contains("ScriptExecutionThreadCount", result.FailureMessage); } [Fact] public void ZeroTagSubscribeRetryIntervalMs_IsRejected() { var result = Validate(new SiteRuntimeOptions { TagSubscribeRetryIntervalMs = 0 }); Assert.True(result.Failed); Assert.Contains("TagSubscribeRetryIntervalMs", result.FailureMessage); } [Fact] public void NegativeStuckScriptGraceMs_IsRejected() { var result = Validate(new SiteRuntimeOptions { StuckScriptGraceMs = -1 }); Assert.True(result.Failed); Assert.Contains("StuckScriptGraceMs", result.FailureMessage); } [Fact] public void ZeroStreamBufferSize_IsRejected() { var result = Validate(new SiteRuntimeOptions { StreamBufferSize = 0 }); Assert.True(result.Failed); Assert.Contains("StreamBufferSize", result.FailureMessage); } [Fact] public void NegativeStartupBatchDelayMs_IsRejected() { var result = Validate(new SiteRuntimeOptions { StartupBatchDelayMs = -1 }); Assert.True(result.Failed); Assert.Contains("StartupBatchDelayMs", result.FailureMessage); } [Fact] public void ZeroAlarmPublishQueueCapacity_IsRejected() { var result = Validate(new SiteRuntimeOptions { AlarmPublishQueueCapacity = 0 }); Assert.True(result.Failed); Assert.Contains("AlarmPublishQueueCapacity", result.FailureMessage); } // ── WP3.1 options ────────────────────────────────────────────────────────────── /// /// The ceiling is the upper bound of the instance-scaled pool; below the floor it would /// silently shrink the configured pool instead of growing it. /// [Fact] public void ScriptExecutionMaxThreadCountBelowThreadCount_IsRejected() { var result = Validate(new SiteRuntimeOptions { ScriptExecutionThreadCount = 8, ScriptExecutionMaxThreadCount = 4 }); Assert.True(result.Failed); Assert.Contains("ScriptExecutionMaxThreadCount", result.FailureMessage); } [Fact] public void EqualScriptExecutionThreadCountAndMax_IsAccepted() { var result = Validate(new SiteRuntimeOptions { ScriptExecutionThreadCount = 8, ScriptExecutionMaxThreadCount = 8 }); Assert.True(result.Succeeded, result.FailureMessage); } /// A zero gate would park every Expression trigger on the node forever. [Fact] public void ZeroTriggerEvalMaxConcurrency_IsRejected() { var result = Validate(new SiteRuntimeOptions { TriggerEvalMaxConcurrency = 0 }); Assert.True(result.Failed); Assert.Contains("TriggerEvalMaxConcurrency", result.FailureMessage); } [Fact] public void ZeroTriggerEvalTimeoutSeconds_IsRejected() { var result = Validate(new SiteRuntimeOptions { TriggerEvalTimeoutSeconds = 0 }); Assert.True(result.Failed); Assert.Contains("TriggerEvalTimeoutSeconds", result.FailureMessage); } /// A zero cap would shed every trigger, silently disabling all scripts. [Fact] public void ZeroTraceArmWindowSeconds_IsRejected() { // Flow Debugger design 2026-09-05 §5.3: the arm window must be positive. var result = Validate(new SiteRuntimeOptions { TraceArmWindowSeconds = 0 }); Assert.True(result.Failed); Assert.Contains("TraceArmWindowSeconds", result.FailureMessage); Assert.Equal(30, new SiteRuntimeOptions().TraceArmWindowSeconds); } /// A zero cap would drop every log line AND the closing row that reports the drops. [Fact] public void ZeroMaxLogLinesPerRun_IsRejected() { // Flow Debugger step logs (design 2026-09-09 D7): the per-run cap must be positive. var result = Validate(new SiteRuntimeOptions { MaxLogLinesPerRun = 0 }); Assert.True(result.Failed); Assert.Contains("MaxLogLinesPerRun", result.FailureMessage); Assert.Equal(200, new SiteRuntimeOptions().MaxLogLinesPerRun); } [Fact] public void ZeroMaxConcurrentRunsPerScript_IsRejected() { var result = Validate(new SiteRuntimeOptions { MaxConcurrentRunsPerScript = 0 }); Assert.True(result.Failed); Assert.Contains("MaxConcurrentRunsPerScript", result.FailureMessage); } // --- Lever 1 (2026-08-28): tag-subscribe backoff + in-flight ceiling --- [Fact] public void TagSubscribeRetryMaxIntervalMs_BelowFloor_IsRejected() { var result = Validate(new SiteRuntimeOptions { TagSubscribeRetryIntervalMs = 5000, TagSubscribeRetryMaxIntervalMs = 4999, }); Assert.True(result.Failed); Assert.Contains("TagSubscribeRetryMaxIntervalMs", result.FailureMessage); } [Fact] public void TagSubscribeRetryMaxIntervalMs_EqualToFloor_IsValid() { var result = Validate(new SiteRuntimeOptions { TagSubscribeRetryIntervalMs = 5000, TagSubscribeRetryMaxIntervalMs = 5000, }); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void ZeroTagSubscribeInFlightCeilingMs_IsRejected() { var result = Validate(new SiteRuntimeOptions { TagSubscribeInFlightCeilingMs = 0 }); Assert.True(result.Failed); Assert.Contains("TagSubscribeInFlightCeilingMs", result.FailureMessage); } }