Merge wt/step-logs-t7: Task 7 Log on compile surface, sandboxes, editor hosts
This commit is contained in:
@@ -28,6 +28,14 @@ public class InboundScriptHost
|
||||
/// </summary>
|
||||
public DatabaseAccessor Database { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Leveled per-run logging. Editor mirror of
|
||||
/// ZB.MOM.WW.ScadaBridge.InboundAPI.InboundScriptLogger (step logs design 2026-09-09 D4);
|
||||
/// shares <see cref="SandboxLog"/> with the Test Run sandbox because only the signatures
|
||||
/// matter here — analysis never invokes it.
|
||||
/// </summary>
|
||||
public SandboxLog Log { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The cancellation token for the operation.
|
||||
/// </summary>
|
||||
|
||||
@@ -26,6 +26,13 @@ public class SandboxInboundScriptHost
|
||||
/// <summary>Gets the database accessor; every call throws <see cref="ScriptSandboxException"/> in a test run.</summary>
|
||||
public DatabaseAccessor Database { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of the inbound runtime's <c>Log</c> global (step logs design 2026-09-09 D4).
|
||||
/// Present so a method script that logs BINDS in the Test Run editor; lines go to the Test Run
|
||||
/// console pane and are never audited.
|
||||
/// </summary>
|
||||
public SandboxLog Log { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Mirror of ZB.MOM.WW.ScadaBridge.InboundAPI.InboundDatabaseHelper — signatures match
|
||||
/// the runtime helper so the same user code compiles, but every call throws because a
|
||||
|
||||
@@ -87,6 +87,58 @@ public class SandboxScriptHost
|
||||
/// would show a spurious CS1061 on a script that deploys and runs perfectly.
|
||||
/// </summary>
|
||||
public SandboxAlarmsAccessor Alarms { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of the site runtime's <c>Log</c> global (step logs design 2026-09-09 D4).
|
||||
/// Present so a script that logs BINDS in the Test Run editor; lines go to the Test Run
|
||||
/// console pane and are never audited. Delegates to <see cref="SandboxInstanceContext.Log"/>
|
||||
/// exactly as the runtime's <c>ScriptGlobals.Log => Instance.Log</c> does, so
|
||||
/// <c>Log</c> and <c>Instance.Log</c> are the same object here too.
|
||||
/// </summary>
|
||||
public SandboxLog Log => Instance.Log;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts.ScriptLogger</c> (step logs
|
||||
/// design 2026-09-09 D4). Signature-faithful so the same script body compiles here and at a site,
|
||||
/// but a Test Run has no execution to trace and no audit trail: every line is written to
|
||||
/// <see cref="Console.Out"/>, which the sandbox captures into
|
||||
/// <c>SandboxRunResult.ConsoleOutput</c> (see <see cref="SandboxConsoleCapture"/>) and shows in
|
||||
/// the editor's console pane. Nothing here reaches the node log or the audit log.
|
||||
/// </summary>
|
||||
public class SandboxLog
|
||||
{
|
||||
/// <summary>Writes a debug-level line to the Test Run console pane.</summary>
|
||||
/// <param name="message">The message text.</param>
|
||||
/// <param name="exception">An optional exception, rendered on a following line.</param>
|
||||
public void Debug(string message, Exception? exception = null) => Write("debug", message, exception);
|
||||
|
||||
/// <summary>Writes an information-level line to the Test Run console pane.</summary>
|
||||
/// <param name="message">The message text.</param>
|
||||
/// <param name="exception">An optional exception, rendered on a following line.</param>
|
||||
public void Info(string message, Exception? exception = null) => Write("info", message, exception);
|
||||
|
||||
/// <summary>Writes a warning-level line to the Test Run console pane.</summary>
|
||||
/// <param name="message">The message text.</param>
|
||||
/// <param name="exception">An optional exception, rendered on a following line.</param>
|
||||
public void Warn(string message, Exception? exception = null) => Write("warn", message, exception);
|
||||
|
||||
/// <summary>
|
||||
/// Writes an error-level line to the Test Run console pane. Like the runtime logger, this
|
||||
/// does NOT fail the run — it is a message, not a throw.
|
||||
/// </summary>
|
||||
/// <param name="message">The message text.</param>
|
||||
/// <param name="exception">An optional exception, rendered on a following line.</param>
|
||||
public void Error(string message, Exception? exception = null) => Write("error", message, exception);
|
||||
|
||||
private static void Write(string level, string message, Exception? exception)
|
||||
{
|
||||
Console.Out.WriteLine($"[{level}] {message}");
|
||||
if (exception != null)
|
||||
{
|
||||
Console.Out.WriteLine(exception.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -193,6 +245,12 @@ public class SandboxInstanceContext
|
||||
/// </summary>
|
||||
public SandboxScriptCallHelper Scripts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sandbox mirror of <c>ScriptRuntimeContext.Log</c> (step logs design 2026-09-09 D4), so
|
||||
/// <c>Instance.Log.Info("…")</c> binds and prints in the Test Run console pane.
|
||||
/// </summary>
|
||||
public SandboxLog Log { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SandboxInstanceContext.
|
||||
/// </summary>
|
||||
|
||||
@@ -67,6 +67,9 @@ public sealed class ScriptCompileSurface
|
||||
/// <summary>Mirrors <c>ScriptGlobals.Alarms</c>.</summary>
|
||||
public CompileAlarmsAccessor Alarms => throw new NotSupportedException(CompileOnly);
|
||||
|
||||
/// <summary>Mirrors <c>ScriptGlobals.Log</c>.</summary>
|
||||
public CompileLog Log => throw new NotSupportedException(CompileOnly);
|
||||
|
||||
/// <summary>Compile-only mirror of <c>ScriptRuntimeContext</c> (the <c>Instance</c> global).</summary>
|
||||
public sealed class CompileInstance
|
||||
{
|
||||
@@ -104,6 +107,38 @@ public sealed class ScriptCompileSurface
|
||||
|
||||
/// <summary>Mirrors <c>ScriptRuntimeContext.Alarms</c>.</summary>
|
||||
public CompileAlarmsAccessor Alarms => throw new NotSupportedException(CompileOnly);
|
||||
|
||||
/// <summary>Mirrors <c>ScriptRuntimeContext.Log</c>.</summary>
|
||||
public CompileLog Log => throw new NotSupportedException(CompileOnly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile-only mirror of <c>ScriptLogger</c> (step logs design 2026-09-09 D4) — the
|
||||
/// <c>Log</c> global a script writes leveled lines through. The optional
|
||||
/// <c>exception</c> parameter is mirrored so <c>Log.Warn("…", ex)</c> binds at design
|
||||
/// time exactly as it does at the site.
|
||||
/// </summary>
|
||||
public sealed class CompileLog
|
||||
{
|
||||
/// <summary>Mirrors <c>ScriptLogger.Debug</c>.</summary>
|
||||
/// <param name="message">The message text.</param>
|
||||
/// <param name="exception">An optional exception to attach to the line.</param>
|
||||
public void Debug(string message, Exception? exception = null) => throw new NotSupportedException(CompileOnly);
|
||||
|
||||
/// <summary>Mirrors <c>ScriptLogger.Info</c>.</summary>
|
||||
/// <param name="message">The message text.</param>
|
||||
/// <param name="exception">An optional exception to attach to the line.</param>
|
||||
public void Info(string message, Exception? exception = null) => throw new NotSupportedException(CompileOnly);
|
||||
|
||||
/// <summary>Mirrors <c>ScriptLogger.Warn</c>.</summary>
|
||||
/// <param name="message">The message text.</param>
|
||||
/// <param name="exception">An optional exception to attach to the line.</param>
|
||||
public void Warn(string message, Exception? exception = null) => throw new NotSupportedException(CompileOnly);
|
||||
|
||||
/// <summary>Mirrors <c>ScriptLogger.Error</c>.</summary>
|
||||
/// <param name="message">The message text.</param>
|
||||
/// <param name="exception">An optional exception to attach to the line.</param>
|
||||
public void Error(string message, Exception? exception = null) => throw new NotSupportedException(CompileOnly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+68
@@ -574,6 +574,40 @@ public class ScriptAnalysisServiceTests
|
||||
Assert.Contains("hello-sandbox", result.ConsoleOutput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunInSandbox_LogGlobal_WritesToConsoleOutput()
|
||||
{
|
||||
// Flow Debugger step logs (design 2026-09-09 D4): the sandbox `Log` mirror exists so
|
||||
// a script that logs BINDS in the Test Run editor, and its lines land in the console
|
||||
// pane (the sandbox has no audit trail — nothing here is written to AuditLog).
|
||||
var result = await _svc.RunInSandboxAsync(
|
||||
new SandboxRunRequest(
|
||||
"Log.Info(\"hello\"); return 1;",
|
||||
Parameters: null,
|
||||
TimeoutSeconds: null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success, result.Error);
|
||||
Assert.Contains("[info] hello", result.ConsoleOutput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunInSandbox_InboundScript_LogGlobal_WritesToConsoleOutput()
|
||||
{
|
||||
// Same mirror on the inbound Test Run host (SandboxInboundScriptHost) — an API
|
||||
// method script's Log.* calls must bind and surface in the console pane too.
|
||||
var result = await _svc.RunInSandboxAsync(
|
||||
new SandboxRunRequest(
|
||||
"Log.Info(\"hello\"); return 1;",
|
||||
Parameters: null,
|
||||
TimeoutSeconds: null,
|
||||
Kind: ScriptKind.InboundApi),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success, result.Error);
|
||||
Assert.Contains("[info] hello", result.ConsoleOutput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunInSandbox_ConcurrentRuns_DoNotCrossContaminateConsoleOutput()
|
||||
{
|
||||
@@ -661,6 +695,40 @@ public class ScriptAnalysisServiceTests
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code.StartsWith("SCADA"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InboundScript_Log_DiagnosesClean()
|
||||
{
|
||||
// Flow Debugger step logs (design 2026-09-09 D4): `Log` is a shipped inbound-script
|
||||
// global (InboundScriptContext.Log). The EDITOR host is a third mirror alongside the
|
||||
// compile surface and the Test Run sandbox — without `Log` on InboundScriptHost a
|
||||
// logging method script shows a spurious CS0103 while deploying and running fine.
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "Log.Info(\"x\");\n"
|
||||
+ "Log.Debug(\"d\");\n"
|
||||
+ "Log.Warn(\"w\", new System.Exception(\"boom\"));\n"
|
||||
+ "Log.Error(\"e\");",
|
||||
Kind: ScriptKind.InboundApi));
|
||||
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code.StartsWith("CS"));
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code.StartsWith("SCADA"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SiteScript_Log_DiagnosesClean()
|
||||
{
|
||||
// The other half of GlobalsTypeFor: every non-inbound kind analyses against
|
||||
// SandboxScriptHost, so the same `Log` global must resolve there too — both at
|
||||
// the top level and via `Instance.Log`.
|
||||
var resp = _svc.Diagnose(new DiagnoseRequest(
|
||||
Code: "Log.Info(\"x\");\n"
|
||||
+ "Log.Warn(\"w\", new System.Exception(\"boom\"));\n"
|
||||
+ "Instance.Log.Info(\"via instance\");",
|
||||
Kind: ScriptKind.Template));
|
||||
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code.StartsWith("CS"));
|
||||
Assert.DoesNotContain(resp.Markers, m => m.Code.StartsWith("SCADA"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InboundScript_Database_DiagnosesClean()
|
||||
{
|
||||
|
||||
@@ -139,6 +139,25 @@ public class RoslynScriptCompilerTests
|
||||
Assert.Empty(diagnostics);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompilesCleanly_ForTheLogGlobal()
|
||||
{
|
||||
// Flow Debugger step logs (design 2026-09-09 D4): `Log` is a script-reachable
|
||||
// global on the runtime ScriptGlobals AND on ScriptRuntimeContext, so both the
|
||||
// top-level `Log` and `Instance.Log` must bind at design time — otherwise a
|
||||
// script that logs would pass the editor but fail the deploy gate.
|
||||
const string code = """
|
||||
Log.Info("a");
|
||||
Log.Warn("b", new System.Exception("x"));
|
||||
Log.Error("c");
|
||||
Log.Debug("d");
|
||||
Instance.Log.Info("e");
|
||||
""";
|
||||
|
||||
var diagnostics = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
|
||||
Assert.Empty(diagnostics);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compile_TwiceWithSharedResolver_StillCompilesCleanly()
|
||||
{
|
||||
|
||||
@@ -212,6 +212,24 @@ public class ScriptTrustValidatorTests
|
||||
Assert.Empty(ScriptTrustValidator.FindViolations(code));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Allows_TheLogGlobal()
|
||||
{
|
||||
// Flow Debugger step logs (design 2026-09-09 D4). The trust model is a DENY-list
|
||||
// over API roots, not an allow-list of context members, so the `Log` global needs
|
||||
// no policy entry — including the overload that attaches a System.Exception, which
|
||||
// is a permitted BCL type. This pins that a logging script is not blocked.
|
||||
const string code = """
|
||||
Log.Info("x");
|
||||
Log.Debug("d");
|
||||
Log.Warn("w", new System.Exception("boom"));
|
||||
Log.Error("e");
|
||||
Instance.Log.Info("via instance");
|
||||
""";
|
||||
|
||||
Assert.Empty(ScriptTrustValidator.FindViolations(code));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForbiddenScopes_DoNotCover_TheCommonsScriptSurface()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user