feat(scripts): Log global on the compile surface and both Test Run sandboxes; trust-gate pin (D4)

This commit is contained in:
Joseph Doherty
2026-09-09 12:21:23 -04:00
parent 964bb9b2d7
commit b2418b41e1
6 changed files with 169 additions and 0 deletions
@@ -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()
{
@@ -139,6 +139,25 @@ public class RoslynScriptCompilerTests
Assert.Empty(diagnostics);
}
[Fact]
public void Compile_Empty_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()
{