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; } }