test(app): add canonical connected soak snapshots

Make scripted lifecycle checkpoints acknowledged post-diagnostics render barriers, capture the exact frame outcome beside canonical resource ownership, and harden the nine-stop route with ordered same-location cache and lifetime gates without weakening process residency thresholds.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 20:01:06 +02:00
parent 2862622ba2
commit bca4148739
17 changed files with 995 additions and 52 deletions

View file

@ -39,7 +39,11 @@ public sealed class FrameRootCompositionTests
source,
"new RenderFrameResourceController(",
"new WorldSceneRenderer(",
"new WorldLifecycleAutomationController(",
"\"world lifecycle automation owner\"",
"\"world lifecycle automation binding\"",
"new RenderFrameOrchestrator(",
"(IRenderFramePostDiagnosticsPhase?)lifecycleAutomation",
"new RetailLiveFrameCoordinator(",
"new UpdateFrameOrchestrator(",
"d.FrameGraphs.PublishOwned(",
@ -62,6 +66,7 @@ public sealed class FrameRootCompositionTests
Assert.DoesNotContain("new AcDream.App.Rendering.WorldSceneRenderer(", source);
Assert.DoesNotContain("new AcDream.App.Update.UpdateFrameOrchestrator(", source);
Assert.DoesNotContain("CaptureWorldLifecycleResourceSnapshot", source);
Assert.DoesNotContain("_worldLifecycleAutomation", source);
Assert.DoesNotContain("_frameGraphs.Publish(", source);
}
@ -83,6 +88,9 @@ public sealed class FrameRootCompositionTests
Assert.Contains("_liveEntities.PendingTeardownCount", snapshots);
Assert.Contains("GpuMemoryTracker.AllocatedBytes", snapshots);
Assert.Contains("_frameProfiler.LastReport", snapshots);
Assert.Contains("Capture(RenderFrameOutcome outcome)", snapshots);
Assert.Contains("outcome.World.VisibleLandblocks", snapshots);
Assert.Contains("outcome.World.TotalLandblocks", snapshots);
}
private sealed class RetryBinding(

View file

@ -110,13 +110,14 @@ public sealed class InteractionUiRuntimeSourcesTests
public void AutomationProxyIsInertUntilExactRuntimeBinds()
{
var source = new DeferredWorldLifecycleAutomationRuntime();
Assert.False(source.TryWriteCheckpoint("early", out string earlyError));
Assert.False(source.TryRequestCheckpoint(
"early", out _, out string earlyError));
Assert.Contains("not bound", earlyError);
var target = new AutomationRuntime();
IDisposable binding = source.Bind(target);
Assert.True(source.IsWorldReady);
Assert.True(source.TryWriteCheckpoint("ready", out _));
Assert.True(source.TryRequestCheckpoint("ready", out _, out _));
Assert.Equal("ready", target.LastCheckpoint);
binding.Dispose();
@ -198,18 +199,35 @@ public sealed class InteractionUiRuntimeSourcesTests
private sealed class AutomationRuntime
: AcDream.App.UI.Testing.IRetailUiAutomationRuntime
{
private sealed record CompletedCheckpoint(int Sequence, string Name)
: AcDream.App.UI.Testing.IRetailUiAutomationCheckpoint
{
public AcDream.App.UI.Testing.RetailUiAutomationCheckpointStatus Status =>
AcDream.App.UI.Testing.RetailUiAutomationCheckpointStatus.Succeeded;
public string? Error => null;
}
public bool IsWorldReady => true;
public bool IsWorldViewportVisible => true;
public int PortalMaterializationCount => 2;
public string? LastCheckpoint { get; private set; }
public bool TryWriteCheckpoint(string name, out string error)
public bool TryRequestCheckpoint(
string name,
out AcDream.App.UI.Testing.IRetailUiAutomationCheckpoint? checkpoint,
out string error)
{
LastCheckpoint = name;
checkpoint = new CompletedCheckpoint(1, name);
error = string.Empty;
return true;
}
public void CancelCheckpoint(
AcDream.App.UI.Testing.IRetailUiAutomationCheckpoint checkpoint)
{
}
public bool TryRequestScreenshot(string name, out string error)
{
error = string.Empty;

View file

@ -0,0 +1,133 @@
namespace AcDream.App.Tests.Diagnostics;
public sealed class ConnectedR6SoakContractTests
{
private static readonly string[] ExpectedCheckpointNames =
[
"caul-baseline",
"sawato-baseline",
"rynthid",
"aerlinthe",
"sawato-return",
"holtburg",
"caul-return",
"sawato-plateau",
"caul-plateau",
];
[Fact]
public void RouteContainsExactlyNineOrderedCheckpointBarriers()
{
string[] checkpoints = File.ReadAllLines(Path.Combine(
FindRepoRoot(),
"tools",
"connected-r6-soak.route.txt"))
.Select(static line => line.Trim())
.Where(static line => line.StartsWith(
"checkpoint ",
StringComparison.Ordinal))
.Select(static line => line["checkpoint ".Length..])
.ToArray();
Assert.Equal(ExpectedCheckpointNames, checkpoints);
}
[Fact]
public void GateConsumesCanonicalTimelineWithoutWeakeningResidencyFormulas()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"tools",
"run-connected-r6-soak.ps1"));
AssertAppearsInOrder(
source,
"$expectedCheckpointNames = @(",
"'caul-baseline'",
"'sawato-baseline'",
"'rynthid'",
"'aerlinthe'",
"'sawato-return'",
"'holtburg'",
"'caul-return'",
"'sawato-plateau'",
"'caul-plateau'");
AssertAppearsInOrder(
source,
"$canonicalCheckpoints = @(Read-CanonicalCheckpoints)",
"Add-CanonicalCheckpointFailures $process",
"Add-RelativeGateFailures");
Assert.Contains("CanonicalCheckpoints = @($canonicalCheckpoints)", source);
Assert.Contains("CheckpointTimeline = $checkpointTimeline", source);
Assert.Contains(
"Join-Path $artifactDir \"checkpoint-$expectedName.json\"",
source,
StringComparison.Ordinal);
Assert.Contains(
"missing render-world synchronization fields",
source,
StringComparison.Ordinal);
string[] zeroFields =
[
"pendingLiveTeardowns",
"pendingLandblockRetirements",
"stagedMeshUploads",
"stagedMeshBytes",
"compositeWarmupPending",
];
foreach (string field in zeroFields)
Assert.Contains($"'{field}'", source, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(
source,
"$privateLimit = [Math]::Max(192.0, $pair.First.PrivateMiB * 0.20)"));
Assert.Equal(1, CountOccurrences(
source,
"$workingLimit = [Math]::Max(192.0, $pair.First.WorkingSetMiB * 0.20)"));
Assert.Equal(1, CountOccurrences(
source,
"$updateLimit = $pair.First.UpdateP95Ms * 1.5 + 0.5"));
Assert.Equal(1, CountOccurrences(
source,
"$allocLimit = $pair.First.AllocP50Kb * 1.5 + 16.0"));
}
private static int CountOccurrences(string source, string value)
{
int count = 0;
int cursor = 0;
while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0)
{
count++;
cursor += value.Length;
}
return count;
}
private static void AssertAppearsInOrder(string source, params string[] values)
{
int cursor = -1;
foreach (string value in values)
{
int next = source.IndexOf(value, cursor + 1, StringComparison.Ordinal);
Assert.True(next >= 0, $"Missing expected source fragment: {value}");
Assert.True(next > cursor, $"Out-of-order source fragment: {value}");
cursor = next;
}
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -1,12 +1,21 @@
using System.Text.Json;
using AcDream.App.Diagnostics;
using AcDream.App.Rendering;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
using SixLabors.ImageSharp;
namespace AcDream.App.Tests.Diagnostics;
public sealed class WorldLifecycleAutomationControllerTests
{
private static readonly RenderFrameInput FrameInput = new(1d / 60d, 1280, 720);
private static readonly RenderFrameOutcome FrameOutcome = new(
new WorldRenderFrameOutcome(4, 17, NormalWorldDrawn: true),
new PrivatePresentationFrameOutcome(
PortalViewportDrawn: false,
ScreenshotCaptured: true));
[Fact]
public void ScreenshotCapture_FlipsGlRowsAndWritesACompletePng()
{
@ -112,7 +121,7 @@ public sealed class WorldLifecycleAutomationControllerTests
var controller = new WorldLifecycleAutomationController(
() => reveal,
() => 3,
() => resources,
_ => resources,
screenshots,
directory);
@ -121,13 +130,26 @@ public sealed class WorldLifecycleAutomationControllerTests
Assert.True(controller.IsWorldReady);
Assert.True(controller.IsWorldViewportVisible);
Assert.Equal(3, controller.PortalMaterializationCount);
Assert.True(controller.TryWriteCheckpoint("dungeon", out string error), error);
Assert.True(controller.TryRequestCheckpoint(
"dungeon",
out IRetailUiAutomationCheckpoint? request,
out string error), error);
Assert.NotNull(request);
Assert.Equal(RetailUiAutomationCheckpointStatus.Pending, request.Status);
controller.Process(FrameInput, FrameOutcome);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, request.Status);
string line = Assert.Single(File.ReadAllLines(
Path.Combine(directory, "world-lifecycle.checkpoints.jsonl")));
using JsonDocument json = JsonDocument.Parse(line);
Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString());
Assert.Equal(7, json.RootElement.GetProperty("reveal").GetProperty("generation").GetInt64());
Assert.Equal(4, json.RootElement.GetProperty("render").GetProperty("world")
.GetProperty("visibleLandblocks").GetInt32());
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
.GetProperty("screenshotCaptured").GetBoolean());
Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
Assert.True(File.Exists(Path.Combine(directory, "checkpoint-dungeon.json")));
}
@ -137,6 +159,123 @@ public sealed class WorldLifecycleAutomationControllerTests
}
}
[Fact]
public void CheckpointRequests_DrainInFifoOrderOnce()
{
string directory = NewDirectory();
int captures = 0;
var controller = CreateController(
directory,
_ =>
{
captures++;
return EmptyResources();
});
try
{
Assert.True(controller.TryRequestCheckpoint(
"first", out IRetailUiAutomationCheckpoint? first, out _));
Assert.True(controller.TryRequestCheckpoint(
"second", out IRetailUiAutomationCheckpoint? second, out _));
controller.Process(FrameInput, FrameOutcome);
controller.Process(FrameInput, FrameOutcome);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(1, first.Sequence);
Assert.Equal(2, second.Sequence);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, first.Status);
Assert.Equal(RetailUiAutomationCheckpointStatus.Succeeded, second.Status);
Assert.Equal(2, captures);
string[] lines = File.ReadAllLines(
Path.Combine(directory, "world-lifecycle.checkpoints.jsonl"));
Assert.Equal(2, lines.Length);
Assert.Contains("\"name\":\"first\"", lines[0], StringComparison.Ordinal);
Assert.Contains("\"name\":\"second\"", lines[1], StringComparison.Ordinal);
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void CheckpointWriteFailure_IsReportedOnTokenWithoutThrowingFromFrame()
{
string directory = NewDirectory();
string blockedPath = Path.Combine(directory, "not-a-directory");
File.WriteAllText(blockedPath, "occupied");
var controller = CreateController(blockedPath, _ => EmptyResources());
try
{
Assert.True(controller.TryRequestCheckpoint(
"failed", out IRetailUiAutomationCheckpoint? request, out _));
controller.Process(FrameInput, FrameOutcome);
Assert.NotNull(request);
Assert.Equal(RetailUiAutomationCheckpointStatus.Failed, request.Status);
Assert.Contains("checkpoint 'failed' sequence 1 failed", request.Error);
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void CheckpointCancellationAndShutdown_AreExplicitAndNeverWritten()
{
string directory = NewDirectory();
var controller = CreateController(directory, _ => EmptyResources());
try
{
Assert.True(controller.TryRequestCheckpoint(
"caller", out IRetailUiAutomationCheckpoint? caller, out _));
Assert.True(controller.TryRequestCheckpoint(
"shutdown", out IRetailUiAutomationCheckpoint? shutdown, out _));
Assert.NotNull(caller);
Assert.NotNull(shutdown);
controller.CancelCheckpoint(caller);
controller.Dispose();
controller.Process(FrameInput, FrameOutcome);
Assert.Equal(RetailUiAutomationCheckpointStatus.Cancelled, caller.Status);
Assert.Contains("cancelled before render-frame capture", caller.Error);
Assert.Equal(RetailUiAutomationCheckpointStatus.Cancelled, shutdown.Status);
Assert.Contains("cancelled during world lifecycle automation shutdown", shutdown.Error);
Assert.False(controller.TryRequestCheckpoint(
"late", out IRetailUiAutomationCheckpoint? late, out string error));
Assert.Null(late);
Assert.Contains("shutting down", error);
Assert.False(File.Exists(Path.Combine(
directory,
"world-lifecycle.checkpoints.jsonl")));
}
finally
{
controller.Dispose();
Directory.Delete(directory, recursive: true);
}
}
private static WorldLifecycleAutomationController CreateController(
string directory,
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> capture) =>
new(
() => default,
() => 0,
capture,
new FrameScreenshotController((_, _) => [], directory),
directory);
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0L, 0, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L, 0d, 0d, null);

View file

@ -38,7 +38,10 @@ public sealed class RenderFrameOrchestratorTests
RenderFrameOutcome outcome = orchestrator.Render(Input);
Assert.Equal(
["gpu-begin", "resources", "world", "presentation", "diagnostics", "gpu-end"],
[
"gpu-begin", "resources", "world", "presentation",
"diagnostics", "post-diagnostics", "gpu-end",
],
calls);
Assert.Equal(phases.World, outcome.World);
Assert.Equal(phases.Presentation, outcome.Presentation);
@ -48,6 +51,7 @@ public sealed class RenderFrameOrchestratorTests
("world", Input),
("presentation", Input),
("diagnostics", Input),
("post-diagnostics", Input),
],
phases.ObservedInputs);
Assert.Equal(phases.World, phases.ObservedWorld);
@ -77,6 +81,7 @@ public sealed class RenderFrameOrchestratorTests
[InlineData("world")]
[InlineData("presentation")]
[InlineData("diagnostics")]
[InlineData("post-diagnostics")]
public void RenderFailure_ClosesExactlyOnceAndPropagates(string failurePoint)
{
var calls = new List<string>();
@ -112,7 +117,10 @@ public sealed class RenderFrameOrchestratorTests
Assert.Same(expected, actual);
Assert.Equal(
["gpu-begin", "resources", "world", "presentation", "diagnostics", "gpu-end"],
[
"gpu-begin", "resources", "world", "presentation",
"diagnostics", "post-diagnostics", "gpu-end",
],
calls);
}
@ -121,6 +129,7 @@ public sealed class RenderFrameOrchestratorTests
[InlineData("world")]
[InlineData("presentation")]
[InlineData("diagnostics")]
[InlineData("post-diagnostics")]
public void RenderAndCloseFailure_AreAggregatedInCausalOrder(string failurePoint)
{
var calls = new List<string>();
@ -208,17 +217,19 @@ public sealed class RenderFrameOrchestratorTests
var phases = new RecordingPhases([]);
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
null!, phases, phases, phases, phases, phases));
null!, phases, phases, phases, phases, phases, phases));
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
phases, null!, phases, phases, phases, phases));
phases, null!, phases, phases, phases, phases, phases));
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
phases, phases, null!, phases, phases, phases));
phases, phases, null!, phases, phases, phases, phases));
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
phases, phases, phases, null!, phases, phases));
phases, phases, phases, null!, phases, phases, phases));
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
phases, phases, phases, phases, null!, phases));
phases, phases, phases, phases, null!, phases, phases));
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
phases, phases, phases, phases, phases, null!));
phases, phases, phases, phases, phases, null!, phases));
Assert.Throws<ArgumentNullException>(() => new RenderFrameOrchestrator(
phases, phases, phases, phases, phases, phases, null!));
}
[Fact]
@ -231,6 +242,7 @@ public sealed class RenderFrameOrchestratorTests
typeof(IWorldSceneFramePhase),
typeof(IPrivatePresentationFramePhase),
typeof(IRenderFrameDiagnosticsPhase),
typeof(IRenderFramePostDiagnosticsPhase),
typeof(IRenderFrameFailureRecovery),
];
FieldInfo[] fields = typeof(RenderFrameOrchestrator).GetFields(
@ -308,6 +320,11 @@ public sealed class RenderFrameOrchestratorTests
"gpu-begin", "resources", "world", "presentation", "diagnostics",
"recovery-abort", "gpu-end",
],
"post-diagnostics" =>
[
"gpu-begin", "resources", "world", "presentation", "diagnostics",
"post-diagnostics", "recovery-abort", "gpu-end",
],
_ => throw new ArgumentOutOfRangeException(nameof(failurePoint)),
};
@ -320,13 +337,18 @@ public sealed class RenderFrameOrchestratorTests
"world" => ["resources", "world"],
"presentation" => ["resources", "world", "presentation"],
"diagnostics" => ["resources", "world", "presentation", "diagnostics"],
"post-diagnostics" =>
[
"resources", "world", "presentation", "diagnostics",
"post-diagnostics",
],
_ => throw new ArgumentOutOfRangeException(nameof(failurePoint)),
};
return phases.Select(phase => (phase, Input)).ToArray();
}
private static RenderFrameOrchestrator Create(RecordingPhases phases) =>
new(phases, phases, phases, phases, phases, phases);
new(phases, phases, phases, phases, phases, phases, phases);
private sealed class RecordingPhases :
IRenderFrameLifetime,
@ -334,6 +356,7 @@ public sealed class RenderFrameOrchestratorTests
IWorldSceneFramePhase,
IPrivatePresentationFramePhase,
IRenderFrameDiagnosticsPhase,
IRenderFramePostDiagnosticsPhase,
IRenderFrameFailureRecovery
{
private readonly List<string> _calls;
@ -399,6 +422,13 @@ public sealed class RenderFrameOrchestratorTests
Record("diagnostics");
}
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
{
ObservedInputs.Add(("post-diagnostics", input));
ObservedOutcome = outcome;
Record("post-diagnostics");
}
private void Record(string call)
{
_calls.Add(call);

View file

@ -37,6 +37,7 @@ public sealed class RenderFrameRecoveryIntegrationTests
new World(failure),
presentation,
new Diagnostics(),
NullRenderFramePostDiagnosticsPhase.Instance,
devTools);
Assert.Throws<InvalidOperationException>(() => orchestrator.Render(Input));

View file

@ -26,6 +26,18 @@ public sealed class RetailUiAutomationProbeTests
private sealed class FakeRuntime : IRetailUiAutomationRuntime
{
private sealed class FakeCheckpoint(int sequence, string name) :
IRetailUiAutomationCheckpoint
{
public int Sequence { get; } = sequence;
public string Name { get; } = name;
public RetailUiAutomationCheckpointStatus Status { get; set; } =
RetailUiAutomationCheckpointStatus.Pending;
public string? Error { get; set; }
}
private readonly List<FakeCheckpoint> _checkpointTokens = [];
public bool IsWorldReady { get; set; }
public bool IsWorldViewportVisible { get; set; }
public int PortalMaterializationCount { get; set; }
@ -33,13 +45,41 @@ public sealed class RetailUiAutomationProbeTests
public HashSet<string> ScreenshotRequests { get; } = new();
public HashSet<string> CompletedScreenshots { get; } = new();
public bool TryWriteCheckpoint(string name, out string error)
public bool TryRequestCheckpoint(
string name,
out IRetailUiAutomationCheckpoint? checkpoint,
out string error)
{
Checkpoints.Add(name);
var token = new FakeCheckpoint(_checkpointTokens.Count + 1, name);
_checkpointTokens.Add(token);
checkpoint = token;
error = string.Empty;
return true;
}
public void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint)
{
var token = Assert.IsType<FakeCheckpoint>(checkpoint);
token.Status = RetailUiAutomationCheckpointStatus.Cancelled;
token.Error = $"checkpoint '{token.Name}' cancelled";
}
public void CompleteCheckpoint(
string name,
RetailUiAutomationCheckpointStatus status =
RetailUiAutomationCheckpointStatus.Succeeded,
string? error = null)
{
FakeCheckpoint token = Assert.Single(
_checkpointTokens,
value => value.Name == name);
if (token.Status != RetailUiAutomationCheckpointStatus.Pending)
throw new InvalidOperationException("checkpoint is already terminal");
token.Status = status;
token.Error = error;
}
public bool TryRequestScreenshot(string name, out string error)
{
ScreenshotRequests.Add(name);
@ -590,6 +630,16 @@ public sealed class RetailUiAutomationProbeTests
runner.Tick(0.001d);
Assert.Equal(["stable"], runtime.Checkpoints);
Assert.DoesNotContain("stable", runtime.ScreenshotRequests);
Assert.False(runner.Completed);
runner.Tick(0.001d);
Assert.Equal(["stable"], runtime.Checkpoints);
Assert.DoesNotContain("stable", runtime.ScreenshotRequests);
runtime.CompleteCheckpoint("stable");
runner.Tick(0.001d);
Assert.Contains("stable", runtime.ScreenshotRequests);
Assert.False(runner.Completed);
@ -604,6 +654,79 @@ public sealed class RetailUiAutomationProbeTests
}
}
[Theory]
[InlineData(RetailUiAutomationCheckpointStatus.Failed, "deferred write failed")]
[InlineData(RetailUiAutomationCheckpointStatus.Cancelled, "shutdown cancelled")]
public void ScriptRunner_checkpointTerminalFailure_StopsWithExactError(
RetailUiAutomationCheckpointStatus status,
string expectedError)
{
var (root, _, _, _, objects) = RootWithTwoItemLists();
var runtime = new FakeRuntime();
var probe = new RetailUiAutomationProbe(root, objects);
var logs = new List<string>();
string path = Path.Combine(
Path.GetTempPath(),
Path.GetRandomFileName() + ".ui-probe.txt");
File.WriteAllLines(path, ["checkpoint stable", "input press CombatToggleCombat"]);
try
{
using var runner = new RetailUiAutomationScriptRunner(
probe,
path,
dumpOnStart: false,
log: logs.Add,
runtime: runtime);
runner.Tick(0d);
runtime.CompleteCheckpoint("stable", status, expectedError);
runner.Tick(0d);
Assert.True(runner.Completed);
Assert.Equal(["stable"], runtime.Checkpoints);
Assert.Contains(logs, line => line.Contains(
expectedError,
StringComparison.Ordinal));
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ScriptRunner_disposeCancelsPendingCheckpoint()
{
var (root, _, _, _, objects) = RootWithTwoItemLists();
var runtime = new FakeRuntime();
var probe = new RetailUiAutomationProbe(root, objects);
string path = Path.Combine(
Path.GetTempPath(),
Path.GetRandomFileName() + ".ui-probe.txt");
File.WriteAllText(path, "checkpoint stable");
try
{
var runner = new RetailUiAutomationScriptRunner(
probe,
path,
dumpOnStart: false,
runtime: runtime);
runner.Tick(0d);
runner.Dispose();
Assert.Equal(["stable"], runtime.Checkpoints);
Assert.Throws<InvalidOperationException>(() =>
runtime.CompleteCheckpoint("stable"));
}
finally
{
File.Delete(path);
}
}
[Fact]
public void ScriptRunner_worldReadyTimeout_stopsWithActionableLine()
{