feat(scripts): realign Test Run with runtime API, add anonymous-object calls and instance binding

The Test Run sandbox and Monaco analysis modelled a script API that had
drifted from the site runtime's ScriptGlobals, so real scripts failed to
compile in Test Run. Realign both to the runtime surface
(Instance/Scripts/ExternalSystem/Attributes/Children/Parent) and drop the
duplicate ScriptHost stub so the two cannot diverge again.

- Script calls (Scripts.CallShared, Instance.CallScript, Route.To().Call)
  accept an anonymous object instead of a hand-built dictionary, via a
  shared ScriptArgs normalizer; existing dictionary calls still compile.
- Test Run can optionally bind to a deployed instance, so Instance/
  Attributes/CallScript route to it cross-site; adds site-side
  RouteToGetAttributes/RouteToSetAttributes handlers.
- Adds Test Run panels to the API method and template script editors.
- Fixes the TestDatabaseQuery seed script, which queried a table that
  never existed.

Also commits unrelated in-progress work already in the tree: the health
monitoring report loop, site streaming changes, and the Admin/Design
data-connection and SMTP page reorganization.
This commit is contained in:
Joseph Doherty
2026-05-16 03:37:56 -04:00
parent d7b05b40e9
commit 295150751f
50 changed files with 2926 additions and 550 deletions

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.Instance;
using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.Commons.Types;
namespace ScadaLink.SiteRuntime.Scripts;
@@ -116,8 +117,10 @@ public class ScriptRuntimeContext
/// Calls a sibling script on the same instance by name (Ask pattern).
/// WP-20: Enforces recursion limit.
/// WP-22: Uses Ask pattern for CallScript.
/// <paramref name="parameters"/> may be a dictionary or an anonymous object
/// (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
/// </summary>
public async Task<object?> CallScript(string scriptName, IReadOnlyDictionary<string, object?>? parameters = null)
public async Task<object?> CallScript(string scriptName, object? parameters = null)
{
var nextDepth = _currentCallDepth + 1;
if (nextDepth > _maxCallDepth)
@@ -131,7 +134,7 @@ public class ScriptRuntimeContext
var correlationId = Guid.NewGuid().ToString();
var request = new ScriptCallRequest(
scriptName,
parameters,
ScriptArgs.Normalize(parameters),
nextDepth,
correlationId);
@@ -200,10 +203,12 @@ public class ScriptRuntimeContext
/// <summary>
/// WP-17: Executes a shared script inline (direct method call, not actor message).
/// WP-20: Enforces recursion limit.
/// <paramref name="parameters"/> may be a dictionary or an anonymous
/// object (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
/// </summary>
public async Task<object?> CallShared(
string scriptName,
IReadOnlyDictionary<string, object?>? parameters = null,
object? parameters = null,
CancellationToken cancellationToken = default)
{
var nextDepth = _currentCallDepth + 1;
@@ -215,7 +220,8 @@ public class ScriptRuntimeContext
throw new InvalidOperationException(msg);
}
return await _library.ExecuteAsync(scriptName, _context, parameters, cancellationToken);
return await _library.ExecuteAsync(
scriptName, _context, ScriptArgs.Normalize(parameters), cancellationToken);
}
}