using Serilog; // resolves Serilog.ILogger explicitly in signatures
using Serilog.Core;
using Serilog.Events;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
///
/// Dry-run harness for the Phase 7 scripting UI. Takes a script + a synthetic input
/// map + evaluates once, returns the output (or rejection / exception) plus any
/// logger emissions the script produced. Per Phase 7 plan decision #22: only inputs
/// the identified can be supplied, so a dependency
/// the harness can't prove statically surfaces as a harness error, not a runtime
/// surprise later.
///
public sealed class ScriptTestHarnessService
{
///
/// Evaluate as a virtual-tag script (return value is the
/// tag's new value). supplies synthetic
/// s for every path the extractor found.
///
public async Task RunVirtualTagAsync(
string source, IDictionary inputs, CancellationToken ct)
{
var deps = DependencyExtractor.Extract(source);
if (!deps.IsValid)
return ScriptTestResult.DependencyRejections(deps.Rejections);
var missing = deps.Reads.Where(r => !inputs.ContainsKey(r)).ToArray();
if (missing.Length > 0)
return ScriptTestResult.MissingInputs(missing);
var extra = inputs.Keys.Where(k => !deps.Reads.Contains(k)).ToArray();
if (extra.Length > 0)
return ScriptTestResult.UnknownInputs(extra);
ScriptEvaluator evaluator;
try
{
evaluator = ScriptEvaluator.Compile(source);
}
catch (Exception compileEx)
{
return ScriptTestResult.Threw(compileEx.Message, []);
}
var capturing = new CapturingSink();
var logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(capturing).CreateLogger();
var ctx = new HarnessVirtualTagContext(inputs, logger);
try
{
var result = await evaluator.RunAsync(ctx, ct);
return ScriptTestResult.Ok(result, ctx.Writes, capturing.Events);
}
catch (Exception ex)
{
return ScriptTestResult.Threw(ex.Message, capturing.Events);
}
}
// Public so Roslyn's script compilation can reference the context type through the
// ScriptGlobals surface. The harness instantiates this directly; operators never see it.
public sealed class HarnessVirtualTagContext(
IDictionary inputs, Serilog.ILogger logger) : ScriptContext
{
public Dictionary Writes { get; } = [];
public override DataValueSnapshot GetTag(string path) =>
inputs.TryGetValue(path, out var v)
? v
: new DataValueSnapshot(null, Ua.StatusCodes.BadNotFound, null, DateTime.UtcNow);
public override void SetVirtualTag(string path, object? value) => Writes[path] = value;
public override DateTime Now => DateTime.UtcNow;
public override Serilog.ILogger Logger => logger;
}
private sealed class CapturingSink : ILogEventSink
{
public List Events { get; } = [];
public void Emit(LogEvent e) => Events.Add(e);
}
}
/// Harness outcome: outputs, write-set, logger events, or a rejection/throw reason.
public sealed record ScriptTestResult(
ScriptTestOutcome Outcome,
object? Output,
IReadOnlyDictionary Writes,
IReadOnlyList LogEvents,
IReadOnlyList Errors)
{
public static ScriptTestResult Ok(object? output, IReadOnlyDictionary writes, IReadOnlyList logs) =>
new(ScriptTestOutcome.Success, output, writes, logs, []);
public static ScriptTestResult Threw(string reason, IReadOnlyList logs) =>
new(ScriptTestOutcome.Threw, null, new Dictionary(), logs, [reason]);
public static ScriptTestResult DependencyRejections(IReadOnlyList rejs) =>
new(ScriptTestOutcome.DependencyRejected, null, new Dictionary(), [],
rejs.Select(r => r.Message).ToArray());
public static ScriptTestResult MissingInputs(string[] paths) =>
new(ScriptTestOutcome.MissingInputs, null, new Dictionary(), [],
paths.Select(p => $"Missing synthetic input: {p}").ToArray());
public static ScriptTestResult UnknownInputs(string[] paths) =>
new(ScriptTestOutcome.UnknownInputs, null, new Dictionary(), [],
paths.Select(p => $"Input '{p}' is not referenced by the script — remove it").ToArray());
}
public enum ScriptTestOutcome
{
Success,
Threw,
DependencyRejected,
MissingInputs,
UnknownInputs,
}
file static class Ua
{
// Mirrors OPC UA StatusCodes.BadNotFound without pulling the OPC stack into Admin.
public static class StatusCodes { public const uint BadNotFound = 0x803E0000; }
}