fix(central-ui): resolve CentralUI-002/003/004 — site-scope enforcement, per-circuit console capture, cached auth state
This commit is contained in:
@@ -465,4 +465,89 @@ public class ScriptAnalysisServiceTests
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("42", result.ReturnValueJson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunInSandbox_CapturesConsoleOutput()
|
||||
{
|
||||
var result = await _svc.RunInSandboxAsync(
|
||||
new SandboxRunRequest(
|
||||
"System.Console.WriteLine(\"hello-sandbox\"); return 1;",
|
||||
Parameters: null,
|
||||
TimeoutSeconds: null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Contains("hello-sandbox", result.ConsoleOutput);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunInSandbox_ConcurrentRuns_DoNotCrossContaminateConsoleOutput()
|
||||
{
|
||||
// Regression test for CentralUI-003. RunInSandboxAsync used to redirect the
|
||||
// process-global Console.Out/Error to a per-call StringWriter. While one run
|
||||
// is mid-flight, any concurrent run's `finally` restores Console.Out to the
|
||||
// ORIGINAL writer — so the long run loses every Console.WriteLine it makes
|
||||
// after that point, and short runs cross-contaminate each other. The fix
|
||||
// routes capture per-call via an AsyncLocal writer without mutating
|
||||
// process-global Console state.
|
||||
|
||||
// A long-running script: writes its tag, then burns CPU, then writes again,
|
||||
// repeatedly. While it spins, many short runs start and finish around it.
|
||||
async Task<string> RunLong()
|
||||
{
|
||||
var code = @"
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
System.Console.WriteLine(""LONG"");
|
||||
long acc = 0;
|
||||
for (long j = 0; j < 2_000_000; j++) acc += j;
|
||||
System.Console.WriteLine(""LONG"" + acc);
|
||||
}
|
||||
return 0;";
|
||||
var r = await _svc.RunInSandboxAsync(
|
||||
new SandboxRunRequest(code, Parameters: null, TimeoutSeconds: 30),
|
||||
CancellationToken.None);
|
||||
Assert.True(r.Success, r.Error);
|
||||
return r.ConsoleOutput;
|
||||
}
|
||||
|
||||
async Task<string> RunShort(int id)
|
||||
{
|
||||
var code = $"for (int i = 0; i < 30; i++) System.Console.WriteLine(\"S{id}\"); return 0;";
|
||||
var r = await _svc.RunInSandboxAsync(
|
||||
new SandboxRunRequest(code, Parameters: null, TimeoutSeconds: 30),
|
||||
CancellationToken.None);
|
||||
Assert.True(r.Success, r.Error);
|
||||
return r.ConsoleOutput;
|
||||
}
|
||||
|
||||
var longTask = RunLong();
|
||||
var shortTasks = new List<Task<string>>();
|
||||
for (var round = 0; round < 12; round++)
|
||||
{
|
||||
for (var k = 0; k < 4; k++)
|
||||
shortTasks.Add(RunShort(round * 4 + k));
|
||||
await Task.Yield();
|
||||
}
|
||||
|
||||
var longOut = await longTask;
|
||||
var shortOuts = await Task.WhenAll(shortTasks);
|
||||
|
||||
// The long run must have captured ALL 80 of its own writes (40 plain + 40 acc).
|
||||
var longLines = longOut.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Count(l => l.StartsWith("LONG"));
|
||||
Assert.Equal(80, longLines);
|
||||
|
||||
// No short run's output must have leaked into the long run's capture.
|
||||
for (var i = 0; i < shortOuts.Length; i++)
|
||||
Assert.DoesNotContain($"S{i}", longOut);
|
||||
|
||||
// Each short run captured exactly its own 30 lines and nothing else.
|
||||
for (var i = 0; i < shortOuts.Length; i++)
|
||||
{
|
||||
var lines = shortOuts[i].Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.Equal(30, lines.Length);
|
||||
Assert.All(lines, l => Assert.Equal($"S{i}", l.Trim()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user