using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
///
/// Runtime globals for the Test Run sandbox. Mirrors the real site-runtime
/// ScriptGlobals surface (ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts) member-for-member
/// so the same user code that runs at a site also compiles and runs here.
///
/// Instance-context members — Instance.GetAttribute/SetAttribute/CallScript ,
/// Attributes , Children , Parent — need a live deployed
/// instance. With no instance bound they throw ;
/// with one bound (see ) they route to it.
///
/// ExternalSystem , Database , and Scripts.CallShared run
/// against central's real services and fire for real; Notify is a
/// signature-faithful no-op fake. None of them depend on a bound instance.
///
public class SandboxScriptHost
{
///
/// Script parameters passed to the sandbox.
///
public ScriptParameters Parameters { get; init; } = new();
///
/// Cancellation token for the sandbox execution.
///
public CancellationToken CancellationToken { get; init; }
///
/// Alarm context for the sandbox.
///
public AlarmContext? Alarm { get; init; }
///
/// Script scope defining the execution context.
///
public ScriptScope Scope { get; init; } = ScriptScope.Root;
///
/// Instance context providing access to deployed instance data.
///
public SandboxInstanceContext Instance { get; init; } = new();
///
/// Helper for external system calls.
///
public SandboxExternalHelper ExternalSystem => Instance.ExternalSystem;
///
/// Helper for database operations.
///
public SandboxDatabaseHelper Database => Instance.Database;
///
/// Helper for sending notifications.
///
public SandboxNotifyHelper Notify => Instance.Notify;
///
/// Helper for calling scripts.
///
public SandboxScriptCallHelper Scripts => Instance.Scripts;
///
/// Accessor for attributes scoped to the current instance.
///
public SandboxAttributeAccessor Attributes => new(Instance, Scope.SelfPath);
///
/// Accessor for child compositions.
///
public SandboxChildrenAccessor Children => new(Instance, Scope.SelfPath);
///
/// Accessor for the parent composition, or null if at root.
///
public SandboxCompositionAccessor? Parent =>
Scope.ParentPath == null ? null : new SandboxCompositionAccessor(Instance, Scope.ParentPath);
///
/// Read-only accessor for the instance's current alarm conditions, mirroring the site
/// runtime's Alarms global (MES alarm-status API §5.2). Present so a script using
/// Alarms.CurrentAsync() BINDS in the Test Run editor — without it the editor
/// would show a spurious CS1061 on a script that deploys and runs perfectly.
///
public SandboxAlarmsAccessor Alarms { get; } = new();
}
///
/// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.AlarmsAccessor .
/// Exists for editor/compile parity only: alarm state is held per-instance by the site's
/// Instance Actor and there is no central Test Run route to it, so an actual call throws a
/// clearly-labelled rather than quietly returning an
/// empty list that would read as "nothing is in alarm".
///
public class SandboxAlarmsAccessor
{
///
/// Mirrors AlarmsAccessor.CurrentAsync for compile parity; unsupported at run
/// time in the central Test Run sandbox.
///
/// Token used to cancel the read (unused in the sandbox).
/// Never returns; always throws in the sandbox.
public Task> CurrentAsync(CancellationToken cancellationToken = default)
=> throw new ScriptSandboxException(
"Alarms.CurrentAsync() reads the site Instance Actor's live alarm conditions, " +
"which aren't available in the central Test Run sandbox — deploy to a site to " +
"exercise alarm-reading scripts.");
}
///
/// Backs the sandbox Instance when a Test Run is bound to a real
/// deployed instance. Null when unbound. The implementation routes to the
/// instance cross-site over the cluster transport.
///
public interface ISandboxInstanceGateway
{
///
/// Gets the value of an attribute with the specified canonical name.
///
/// The canonical name of the attribute.
/// Cancellation token.
/// The attribute value, or null if not found.
Task GetAttributeAsync(string canonicalName, CancellationToken ct);
///
/// Sets the value of an attribute with the specified canonical name.
///
/// The canonical name of the attribute.
/// The value to set.
/// Cancellation token.
/// A task that represents the asynchronous operation.
Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct);
///
/// Calls a script with the specified canonical name.
///
/// The canonical name of the script.
/// Script parameters, or null if none.
/// Cancellation token.
/// The script result, or null if none.
Task CallScriptAsync(
string canonicalScriptName, IReadOnlyDictionary? parameters, CancellationToken ct);
///
/// Blocks until the named attribute reaches a target value (value-equality,
/// codec-encoded), optionally quality-gated, or the timeout elapses — routed to the
/// bound deployed instance cross-site. Value-equality only: a script-side predicate
/// cannot be routed, so the sandbox predicate overloads stay unsupported.
///
/// The (already scope-resolved) canonical attribute name.
/// The AttributeValueCodec -encoded target value, or null for "any change".
/// Maximum time to wait.
/// When true, only a Good-quality value satisfies the wait.
/// Cancellation token.
/// The wait outcome (matched flag, matched value, quality, timed-out flag).
Task WaitForAttributeAsync(
string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality, CancellationToken ct);
}
///
/// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptRuntimeContext —
/// the Instance global. Attribute and sibling-script access needs a real
/// deployed instance: with no gateway wired it throws; with one (a bound
/// instance) it routes cross-site. ExternalSystem /Database /
/// Scripts run against central's real services regardless of binding;
/// Notify is a signature-faithful no-op fake.
///
public class SandboxInstanceContext
{
private readonly ISandboxInstanceGateway? _gateway;
///
/// Helper for external system calls.
///
public SandboxExternalHelper ExternalSystem { get; }
///
/// Helper for database operations.
///
public SandboxDatabaseHelper Database { get; }
///
/// Helper for sending notifications.
///
public SandboxNotifyHelper Notify { get; }
///
/// Helper for calling scripts.
///
public SandboxScriptCallHelper Scripts { get; }
///
/// Initializes a new instance of the SandboxInstanceContext.
///
/// Gateway for accessing deployed instance data, or null if unbound.
/// External system helper, or null to create a default.
/// Database helper, or null to create a default.
/// Notification helper, or null to create a default.
/// Script call helper, or null to create a default.
public SandboxInstanceContext(
ISandboxInstanceGateway? gateway = null,
SandboxExternalHelper? external = null,
SandboxDatabaseHelper? database = null,
SandboxNotifyHelper? notify = null,
SandboxScriptCallHelper? scripts = null)
{
_gateway = gateway;
ExternalSystem = external ?? new SandboxExternalHelper(null, "");
Database = database ?? new SandboxDatabaseHelper(null, "");
Notify = notify ?? new SandboxNotifyHelper();
Scripts = scripts ?? new SandboxScriptCallHelper(null);
}
///
/// Gets the value of an attribute.
///
/// The name of the attribute.
/// The attribute value, or null if not found.
public Task GetAttribute(string attributeName)
{
if (_gateway == null)
throw new ScriptSandboxException(
$"GetAttribute(\"{attributeName}\") needs a deployed instance — " +
"bind one in Test Run to read live attribute values.");
return _gateway.GetAttributeAsync(attributeName, CancellationToken.None);
}
///
/// Sets the value of an attribute.
///
/// The name of the attribute.
/// The value to set.
public void SetAttribute(string attributeName, string value)
{
if (_gateway == null)
throw new ScriptSandboxException(
$"SetAttribute(\"{attributeName}\") needs a deployed instance — " +
"bind one in Test Run to write attribute values.");
_gateway.SetAttributeAsync(attributeName, value, CancellationToken.None).GetAwaiter().GetResult();
}
///
/// Calls a sibling script.
///
/// The name of the script.
/// Script parameters, or null if none.
/// The script result, or null if none.
public Task CallScript(string scriptName, object? parameters = null)
{
if (_gateway == null)
throw new ScriptSandboxException(
$"CallScript(\"{scriptName}\") needs a deployed instance — " +
"bind one in Test Run to call sibling scripts.");
return _gateway.CallScriptAsync(scriptName, ScriptArgs.Normalize(parameters), CancellationToken.None);
}
///
/// Routes a value-equality attribute wait to the bound deployed instance cross-site.
/// Needs a bound instance (the site owns the event-driven waiter); throws when unbound.
///
/// The (already scope-resolved) canonical attribute name.
/// The codec-encoded target value, or null for "any change".
/// Maximum time to wait.
/// When true, only a Good-quality value satisfies the wait.
/// The wait outcome.
public Task WaitForAttribute(
string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality)
{
if (_gateway == null)
throw new ScriptSandboxException(
$"WaitForAttribute(\"{canonicalName}\") needs a deployed instance — " +
"bind one in Test Run to exercise the event-driven attribute waiter.");
return _gateway.WaitForAttributeAsync(
canonicalName, targetValueEncoded, timeout, requireGoodQuality, CancellationToken.None);
}
}
///
/// Sandbox mirror of ScriptRuntimeContext.ScriptCallHelper —
/// Scripts.CallShared(...) . Compiles and runs the named shared script in
/// the same sandbox via the wired delegate.
///
public class SandboxScriptCallHelper
{
private readonly Func?, CancellationToken, Task>? _callShared;
///
/// Initializes a new instance of the SandboxScriptCallHelper.
///
/// Delegate for calling shared scripts, or null if not available.
public SandboxScriptCallHelper(
Func?, CancellationToken, Task>? callShared)
{
_callShared = callShared;
}
///
/// Calls a shared script.
///
/// The name of the shared script.
/// Script parameters, or null if none.
/// Cancellation token.
/// The script result, or null if none.
public Task CallShared(
string scriptName,
object? parameters = null,
CancellationToken cancellationToken = default)
{
if (_callShared == null)
throw new ScriptSandboxException(
$"Scripts.CallShared(\"{scriptName}\") — shared-script catalog not configured for Test Run.");
return _callShared(scriptName, ScriptArgs.Normalize(parameters), cancellationToken);
}
}
///
/// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.AttributeAccessor —
/// scope-aware Attributes["X"] access anchored at a canonical-name prefix.
///
public class SandboxAttributeAccessor
{
private readonly SandboxInstanceContext _ctx;
///
/// The scope prefix for attribute resolution.
///
public string ScopePrefix { get; }
///
/// Initializes a new instance of the SandboxAttributeAccessor.
///
/// The sandbox instance context.
/// The scope prefix for attribute names.
public SandboxAttributeAccessor(SandboxInstanceContext ctx, string prefix)
{
_ctx = ctx;
ScopePrefix = prefix;
}
///
/// Resolves a key to its fully qualified name within the current scope.
///
/// The attribute key.
/// The fully qualified attribute name.
public string Resolve(string key) =>
ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key;
///
/// Gets or sets an attribute value by key.
///
/// The attribute key.
/// The attribute value, or null if not found.
public object? this[string key]
{
get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult();
set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
}
///
/// Gets an attribute value asynchronously.
///
/// The attribute key.
/// The attribute value, or null if not found.
public Task GetAsync(string key) => _ctx.GetAttribute(Resolve(key));
///
/// Sets an attribute value asynchronously.
///
/// The attribute key.
/// The value to set, or null.
/// A task representing the operation.
public Task SetAsync(string key, object? value)
{
_ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
return Task.CompletedTask;
}
// Batch-write/wait helpers. These mirror the runtime AttributeAccessor
// (SiteRuntime/Scripts/ScopeAccessors.cs) and the deploy-gate
// ScriptCompileSurface member-for-member so instance scripts using them COMPILE
// in the editor and pass Test Run analysis — previously the
// sandbox omitted them and the editor false-flagged valid scripts with CS1061.
// Execution needs the site's DCL batch path + event-driven attribute waiter,
// for which the central Test Run sandbox has no transport, so each throws a
// clearly-labelled ScriptSandboxException; the same code validates/deploys/runs
// unchanged at a site.
///
/// Sandbox stand-in for AttributeAccessor.WriteBatchAndWaitAsync : present
/// for editor/compile parity, throws when run
/// in Test Run (no device batch-write transport here).
///
/// The attribute values to write as a batch.
/// The flag attribute key to set to signal completion.
/// The value to set on the flag attribute.
/// The attribute key to observe for the device's response.
/// The response value to wait for.
/// The maximum time to wait for the response.
/// Never returns; always throws in the sandbox.
public Task WriteBatchAndWaitAsync(
IReadOnlyDictionary values, string flagKey, object? flagValue,
string responseKey, object? responseValue, TimeSpan timeout)
=> throw NotInSandbox(nameof(WriteBatchAndWaitAsync));
///
/// Value-equality attribute wait — routed to the bound deployed instance cross-site
/// (the same real event-driven waiter a site uses). Needs a bound instance; throws
/// when the Test Run has no instance bound.
///
/// The attribute key to observe.
/// The value to wait for (codec-normalized), or null for "any change".
/// The maximum time to wait.
/// Whether the attribute must also have good quality to satisfy the wait.
/// True when the attribute reached the target within the timeout; false on timeout.
public async Task WaitAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
=> (await _ctx.WaitForAttribute(
Resolve(key), AttributeValueCodec.Encode(targetValue), timeout, requireGoodQuality)).Matched;
///
/// Predicate-form attribute wait. Unsupported in Test Run because a script-side
/// predicate (an in-process lambda) cannot be evaluated on the remote site — only the
/// value-equality overloads route. Throws a clearly-labelled .
///
/// The attribute key to observe.
/// The predicate the attribute value must satisfy.
/// The maximum time to wait.
/// Whether the attribute must also have good quality to satisfy the wait.
/// Never returns; always throws in the sandbox.
public Task WaitAsync(string key, Func predicate, TimeSpan timeout, bool requireGoodQuality = false)
=> throw PredicateWaitNotRoutable(nameof(WaitAsync));
///
/// Value-equality attribute wait returning the full — routed
/// to the bound deployed instance cross-site. Needs a bound instance; throws
/// when the Test Run has no instance bound.
///
/// The attribute key to observe.
/// The value to wait for (codec-normalized), or null for "any change".
/// The maximum time to wait.
/// Whether the attribute must also have good quality to satisfy the wait.
/// The wait outcome (matched flag, matched value, quality, timed-out flag).
public Task WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false)
=> _ctx.WaitForAttribute(Resolve(key), AttributeValueCodec.Encode(targetValue), timeout, requireGoodQuality);
///
/// Predicate-form attribute wait returning the full .
/// Unsupported in Test Run for the same reason as the predicate
/// overload — an in-process predicate cannot be routed to the remote site.
///
/// The attribute key to observe.
/// The predicate the attribute value must satisfy.
/// The maximum time to wait.
/// Whether the attribute must also have good quality to satisfy the wait.
/// Never returns; always throws in the sandbox.
public Task WaitForAsync(string key, Func predicate, TimeSpan timeout, bool requireGoodQuality = false)
=> throw PredicateWaitNotRoutable(nameof(WaitForAsync));
private static ScriptSandboxException NotInSandbox(string member) =>
new($"{member}(...) drives live device tags and the site's event-driven " +
"attribute waiter, which aren't available in the central Test Run sandbox — " +
"deploy to a site to exercise batch-write/wait handshakes.");
private static ScriptSandboxException PredicateWaitNotRoutable(string member) =>
new($"{member}(...) with a predicate can't run in Test Run — a script-side " +
"predicate is an in-process lambda that cannot be evaluated on the remote " +
"site. Use the value-equality overload (which routes to the bound instance), " +
"or deploy to a site to exercise predicate waits.");
}
///
/// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.CompositionAccessor —
/// a view of one composition: its attributes plus an invokable CallScript .
///
public class SandboxCompositionAccessor
{
private readonly SandboxInstanceContext _ctx;
///
/// The path to the composition within the instance hierarchy.
///
public string Path { get; }
///
/// Accessor for attributes within the composition.
///
public SandboxAttributeAccessor Attributes { get; }
///
/// Initializes a new instance of the SandboxCompositionAccessor.
///
/// The sandbox instance context.
/// The path to the composition within the instance hierarchy.
public SandboxCompositionAccessor(SandboxInstanceContext ctx, string path)
{
_ctx = ctx;
Path = path;
Attributes = new SandboxAttributeAccessor(ctx, path);
}
///
/// Resolves a script name to its fully qualified name within the composition.
///
/// The script name.
/// The fully qualified script name.
public string ResolveScript(string scriptName) =>
Path.Length == 0 ? scriptName : Path + "." + scriptName;
///
/// Calls a script within the composition.
///
/// The name of the script.
/// Script parameters, or null if none.
/// The script result, or null if none.
public Task CallScript(string scriptName, object? parameters = null)
=> _ctx.CallScript(ResolveScript(scriptName), parameters);
}
///
/// Sandbox mirror of ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ChildrenAccessor —
/// dictionary-style access to child compositions.
///
public class SandboxChildrenAccessor
{
private readonly SandboxInstanceContext _ctx;
private readonly string _selfPath;
///
/// Initializes a new instance of the SandboxChildrenAccessor.
///
/// The sandbox instance context.
/// The path to the parent composition.
public SandboxChildrenAccessor(SandboxInstanceContext ctx, string selfPath)
{
_ctx = ctx;
_selfPath = selfPath;
}
///
/// Gets a child composition by name.
///
/// The name of the child composition.
/// An accessor for the child composition.
public SandboxCompositionAccessor this[string compositionName]
{
get
{
var path = _selfPath.Length == 0
? compositionName
: _selfPath + "." + compositionName;
return new SandboxCompositionAccessor(_ctx, path);
}
}
}
///
/// Distinct exception so the Test Run pipeline can label sandbox-only
/// limitations differently from genuine runtime errors in user code.
///
public class ScriptSandboxException : Exception
{
///
/// Initializes a new instance of the ScriptSandboxException.
///
/// The exception message.
public ScriptSandboxException(string message) : base(message) { }
}